From 2370a5ae7ce69371aa14cbb46bb2fb8c291d492e Mon Sep 17 00:00:00 2001 From: Admin Bot Date: Thu, 3 Sep 2026 19:30:39 -0700 Subject: [PATCH] auth: enable JWT validation, support roles claim - Enable auth in gateway config with JWKS URL - Check both 'permissions' (users) and 'roles' (service accounts) - Allow any Authentik issuer (service accounts have per-provider issuers) - Required capability: llm:inference --- internal/auth/jwt.go | 54 ++++++++++++++++++++++++++++++++++---------- k8s/configmap.yaml | 7 +++++- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 3fe0e80..c77b094 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -3,6 +3,7 @@ package auth import ( "context" "fmt" + "strings" "sync" "time" @@ -107,42 +108,64 @@ func (v *Validator) ValidateBearerToken(authHeader string) (jwt.MapClaims, error } } - // Check iss (issuer) - if iss, ok := claims["iss"].(string); !ok || iss != v.issuer { - return nil, fmt.Errorf("invalid issuer: expected %s, got %s", v.issuer, iss) + // Check iss (issuer) - if configured, must match exactly or be from Authentik + // Service accounts have per-provider issuers, so we allow any authentik.riotpiao.com issuer + if v.issuer != "" { + iss, ok := claims["iss"].(string) + if !ok { + return nil, fmt.Errorf("missing issuer claim") + } + // Allow exact match or any Authentik issuer + if iss != v.issuer && !isAuthentikIssuer(iss) { + return nil, fmt.Errorf("invalid issuer: expected %s or Authentik, got %s", v.issuer, iss) + } } - // Check aud (audience) - if aud, ok := claims["aud"].(string); !ok || aud != v.audience { - return nil, fmt.Errorf("invalid audience: expected %s, got %s", v.audience, aud) + // Check aud (audience) - if configured, must match + if v.audience != "" { + if aud, ok := claims["aud"].(string); !ok || aud != v.audience { + return nil, fmt.Errorf("invalid audience: expected %s, got %s", v.audience, aud) + } } return claims, nil } // CheckPermissions checks if claims contain required permission(s). +// Checks both "permissions" (user tokens) and "roles" (service account tokens). // Returns true if any required permission is found or wildcard "*" exists. func (v *Validator) CheckPermissions(claims jwt.MapClaims, required ...string) bool { - permsIface, ok := claims["permissions"] + // Check both claims - users have "permissions", service accounts have "roles" + for _, claimKey := range []string{"permissions", "roles"} { + if v.checkClaimList(claims, claimKey, required...) { + return true + } + } + return false +} + +// checkClaimList checks if a specific claim contains any required value. +func (v *Validator) checkClaimList(claims jwt.MapClaims, claimKey string, required ...string) bool { + valuesIface, ok := claims[claimKey] if !ok { return false } - perms, ok := permsIface.([]interface{}) + values, ok := valuesIface.([]interface{}) if !ok { return false } - for _, perm := range perms { - permStr, ok := perm.(string) + for _, val := range values { + valStr, ok := val.(string) if !ok { continue } - if permStr == "*" { + if valStr == "*" { return true } for _, req := range required { - if permStr == req { + if valStr == req { return true } } @@ -151,6 +174,13 @@ func (v *Validator) CheckPermissions(claims jwt.MapClaims, required ...string) b return false } +// isAuthentikIssuer checks if issuer is from our Authentik instance. +func isAuthentikIssuer(iss string) bool { + return len(iss) > 0 && + (iss == "https://authentik.riotpiao.com" || + strings.HasPrefix(iss, "https://authentik.riotpiao.com/")) +} + // DecodeToken decodes JWT payload without verification (for debugging/testing). func DecodeToken(tokenString string) (jwt.MapClaims, error) { claims := jwt.MapClaims{} diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index 4cd37dd..3de43b4 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -11,8 +11,13 @@ data: # See REQUIREMENTS.md for full specification # JWT Authentication for /v1/* endpoints (LLM API) + # All Authentik providers share same signing key, so any JWKS URL works. + # Issuer varies per provider (portfolio-agent, memory-agent, local-llm, etc.) + # Gateway validates: signature + roles claim contains requiredCapability auth: - enabled: false + enabled: true + jwksUrl: "https://authentik.riotpiao.com/application/o/local-llm/jwks/" + requiredCapability: "llm:inference" # Routes: standard HTTP proxy routes (not LLM-specific) # These are for non-LLM services (agent-pod/console, etc.)