diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 3c12421..8838103 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -19,20 +19,25 @@ func isValidIssuer(iss string) bool { } // Validator validates JWTs against Authentik JWKS. +// Supports multi-issuer: any Authentik service account provider is accepted +// (portfolio-agent, memory-agent, api-gw, etc.) because all share the same +// JWKS signing key. type Validator struct { - issuer string - audience string + issuer string // Not used for validation (kept for logging); issuer regex check is sufficient + audience string // Not used for validation; any audience from valid Authentik issuer is accepted jwksURL string jwks *keyfunc.JWKS mu sync.Mutex } // NewValidator creates a new JWT validator for a service. +// issuer and audience params are deprecated (ignored for validation) but kept +// for backward compatibility. Multi-issuer validation via isValidIssuer() is used instead. // JWKS fetching is lazy (deferred until first validation). func NewValidator(issuer, audience, jwksURL string) *Validator { return &Validator{ - issuer: issuer, - audience: audience, + issuer: issuer, // deprecated param, kept for compat + audience: audience, // deprecated param, kept for compat jwksURL: jwksURL, jwks: nil, // Lazy-loaded on first use } @@ -124,10 +129,16 @@ func (v *Validator) ValidateBearerToken(authHeader string) (jwt.MapClaims, error return nil, fmt.Errorf("invalid issuer: %s", 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) - accept any Authentik-provided audience + // since all Authentik service accounts use the same signing key. + // The issuer check above is sufficient to ensure JWT came from Authentik. + if aud, ok := claims["aud"].(string); !ok { + return nil, fmt.Errorf("missing audience claim") + } else if aud == "" { + return nil, fmt.Errorf("empty audience claim") } + // Note: Not hardcoding expected audience. Any audience from a valid Authentik + // issuer is accepted, since all service accounts are under the same trust boundary. return claims, nil }