auth: enable JWT validation, support roles claim
CI / Vet, test, build (push) Successful in 3m45s
CI / Build and push image (push) Successful in 1m19s

- 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
This commit is contained in:
Admin Bot
2026-09-03 19:30:39 -07:00
parent f154c5993a
commit 2370a5ae7c
2 changed files with 48 additions and 13 deletions
+42 -12
View File
@@ -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{}