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
This commit is contained in:
+42
-12
@@ -3,6 +3,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -107,42 +108,64 @@ func (v *Validator) ValidateBearerToken(authHeader string) (jwt.MapClaims, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check iss (issuer)
|
// Check iss (issuer) - if configured, must match exactly or be from Authentik
|
||||||
if iss, ok := claims["iss"].(string); !ok || iss != v.issuer {
|
// Service accounts have per-provider issuers, so we allow any authentik.riotpiao.com issuer
|
||||||
return nil, fmt.Errorf("invalid issuer: expected %s, got %s", v.issuer, iss)
|
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)
|
// Check aud (audience) - if configured, must match
|
||||||
if aud, ok := claims["aud"].(string); !ok || aud != v.audience {
|
if v.audience != "" {
|
||||||
return nil, fmt.Errorf("invalid audience: expected %s, got %s", v.audience, aud)
|
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
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPermissions checks if claims contain required permission(s).
|
// 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.
|
// Returns true if any required permission is found or wildcard "*" exists.
|
||||||
func (v *Validator) CheckPermissions(claims jwt.MapClaims, required ...string) bool {
|
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 {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
perms, ok := permsIface.([]interface{})
|
values, ok := valuesIface.([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, perm := range perms {
|
for _, val := range values {
|
||||||
permStr, ok := perm.(string)
|
valStr, ok := val.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if permStr == "*" {
|
if valStr == "*" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, req := range required {
|
for _, req := range required {
|
||||||
if permStr == req {
|
if valStr == req {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +174,13 @@ func (v *Validator) CheckPermissions(claims jwt.MapClaims, required ...string) b
|
|||||||
return false
|
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).
|
// DecodeToken decodes JWT payload without verification (for debugging/testing).
|
||||||
func DecodeToken(tokenString string) (jwt.MapClaims, error) {
|
func DecodeToken(tokenString string) (jwt.MapClaims, error) {
|
||||||
claims := jwt.MapClaims{}
|
claims := jwt.MapClaims{}
|
||||||
|
|||||||
+6
-1
@@ -11,8 +11,13 @@ data:
|
|||||||
# See REQUIREMENTS.md for full specification
|
# See REQUIREMENTS.md for full specification
|
||||||
|
|
||||||
# JWT Authentication for /v1/* endpoints (LLM API)
|
# 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:
|
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)
|
# Routes: standard HTTP proxy routes (not LLM-specific)
|
||||||
# These are for non-LLM services (agent-pod/console, etc.)
|
# These are for non-LLM services (agent-pod/console, etc.)
|
||||||
|
|||||||
Reference in New Issue
Block a user