mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-08-27 15:53:32 +08:00
refactor: use crypto approach to store authorize status
This commit is contained in:
@@ -245,18 +245,40 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
cookieId := strings.SplitN(client.ClientID, "-", 2)[0]
|
||||
cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId)
|
||||
scopeCookie, err := c.Cookie(cookieName)
|
||||
checkSkipAuthorize := func() {
|
||||
cookieId := client.ClientID[8:]
|
||||
cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId)
|
||||
scopeCookie, err := c.Cookie(cookieName)
|
||||
|
||||
if err == nil {
|
||||
scopes := fmt.Sprintf("scopes=%s;", req.Scope)
|
||||
if controller.oidc.VerifySignedValue(client.ClientSecret, []byte(scopes), scopeCookie) {
|
||||
if values.OIDCPrompt != service.OIDCPromptLogin {
|
||||
values.OIDCPrompt = service.OIDCPromptNone
|
||||
}
|
||||
if err != nil || userContext == nil || !userContext.Authenticated {
|
||||
return
|
||||
}
|
||||
|
||||
kv, err := controller.oidc.DecryptSecureValue(client.ClientSecret, scopeCookie)
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to decrypt scope cookie")
|
||||
return
|
||||
}
|
||||
|
||||
scope, ok := kv["scope"]
|
||||
if !ok {
|
||||
controller.log.App.Warn().Str("cookieName", cookieName).Msg("Failed to get scopes from scope cookie")
|
||||
return
|
||||
}
|
||||
|
||||
username, ok := kv["username"]
|
||||
if !ok {
|
||||
controller.log.App.Warn().Str("cookieName", cookieName).Msg("Failed to get username from scope cookie")
|
||||
return
|
||||
}
|
||||
|
||||
if username == userContext.GetUsername() &&
|
||||
values.OIDCPrompt != service.OIDCPromptLogin &&
|
||||
scope == req.Scope {
|
||||
values.OIDCPrompt = service.OIDCPromptNone
|
||||
}
|
||||
}
|
||||
checkSkipAuthorize()
|
||||
|
||||
queries, err := query.Values(values)
|
||||
|
||||
@@ -386,21 +408,29 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Set a cookie for the consent screen (approved scopes)
|
||||
cookieId := strings.SplitN(client.ClientID, "-", 2)[0]
|
||||
cookieId := client.ClientID[8:]
|
||||
cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId)
|
||||
scopes := fmt.Sprintf("scopes=%s;", authorizeReq.Scope)
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: controller.oidc.CreateSignedValue(client.ClientSecret, []byte(scopes)),
|
||||
Path: "/",
|
||||
Secure: controller.config.Auth.SecureCookie,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
secureSignedValue, err := controller.oidc.CreateSecureValue(client.ClientSecret, map[string]string{
|
||||
"username": userContext.GetUsername(),
|
||||
"scope": authorizeReq.Scope,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
cookie := &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: secureSignedValue,
|
||||
Path: "/",
|
||||
Secure: controller.config.Auth.SecureCookie,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
} else {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to create scope cookie")
|
||||
}
|
||||
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
|
||||
q := cu.Query()
|
||||
|
||||
q.Set("code", code)
|
||||
|
||||
@@ -24,3 +24,5 @@ const OAuthSessionCookieName = "tinyauth-oauth"
|
||||
const OIDCScopeCookieName = "tinyauth-scope"
|
||||
|
||||
const GracefulShutdownTimeout = 5 // seconds
|
||||
|
||||
const HKDFSalt = "tinyauth-hkdf-salt-v1"
|
||||
|
||||
@@ -3,7 +3,8 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -23,7 +25,6 @@ import (
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/steveiliop56/ding"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"github.com/tinyauthapp/tinyauth/pkg/cache"
|
||||
"go.uber.org/dig"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -308,9 +310,6 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
|
||||
|
||||
for id, client := range i.Config.OIDC.Clients {
|
||||
client.ID = id
|
||||
if err := uuid.Validate(client.ClientID); err != nil {
|
||||
return nil, fmt.Errorf("invalid client id: %w", err)
|
||||
}
|
||||
if client.Name == "" {
|
||||
client.Name = utils.Capitalize(client.ID)
|
||||
}
|
||||
@@ -324,9 +323,6 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
|
||||
client.ClientSecret = secret
|
||||
}
|
||||
client.ClientSecretFile = ""
|
||||
if len(client.ClientSecret) < 32 {
|
||||
return nil, fmt.Errorf("client secret for client %s is too short, must be >= 32 chars", client.ClientID)
|
||||
}
|
||||
clients[id] = client
|
||||
i.Log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration")
|
||||
}
|
||||
@@ -928,7 +924,7 @@ func (service *OIDCService) DeleteAuthorizeRequestTicket(ticket string) {
|
||||
service.caches.authorize.Delete(ticket)
|
||||
}
|
||||
|
||||
// TODO: support signed request objects in the future
|
||||
// DecodeAuthorizeJWT TODO: support signed request objects in the future
|
||||
func (service *OIDCService) DecodeAuthorizeJWT(tokenString string) (*AuthorizeRequest, error) {
|
||||
var claims jwt.MapClaims
|
||||
|
||||
@@ -979,20 +975,82 @@ func (service *OIDCService) GetPrompt(prompt string) []OIDCPrompt {
|
||||
return parsedPromps
|
||||
}
|
||||
|
||||
func (service *OIDCService) CreateSignedValue(key string, data []byte) string {
|
||||
// create the signature
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write(data)
|
||||
sig := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
// hash the data
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
hash := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
return fmt.Sprintf("%s.%s", hash, sig)
|
||||
func (service *OIDCService) deriveKey(key string, info string) ([]byte, error) {
|
||||
kdf := hkdf.New(sha256.New, []byte(key), []byte(model.HKDFSalt), []byte(info))
|
||||
derived := make([]byte, 32)
|
||||
if _, err := io.ReadFull(kdf, derived); err != nil {
|
||||
return nil, fmt.Errorf("failed to derive key: %w", err)
|
||||
}
|
||||
return derived, nil
|
||||
}
|
||||
|
||||
func (service *OIDCService) VerifySignedValue(key string, data []byte, signedValue string) bool {
|
||||
return service.CreateSignedValue(key, data) == signedValue
|
||||
func (service *OIDCService) CreateSecureValue(key string, kv map[string]string) (string, error) {
|
||||
aesKey, err := service.deriveKey(key, "oidc-scope-v1")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create aes cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create gcm: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
plain, err := json.Marshal(kv)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal data: %w", err)
|
||||
}
|
||||
|
||||
sealed := gcm.Seal(nonce, nonce, plain, nil)
|
||||
return base64.RawURLEncoding.EncodeToString(sealed), nil
|
||||
}
|
||||
|
||||
func (service *OIDCService) DecryptSecureValue(key string, encrypted string) (map[string]string, error) {
|
||||
data, err := base64.RawURLEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode value: %w", err)
|
||||
}
|
||||
|
||||
aesKey, err := service.deriveKey(key, "oidc-scope-v1")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create aes cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gcm: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt secure value: %w", err)
|
||||
}
|
||||
|
||||
kv := make(map[string]string) // make(), not var — nil map writes panic
|
||||
if err := json.Unmarshal(plain, &kv); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse secure value: %w", err)
|
||||
}
|
||||
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user