feat(auth): validate access tokens with the ZITADEL Go SDK
Replace the handwritten Authentik JWT checks with github.com/zitadel/zitadel-go/v3. What the SDK now owns: - OIDC discovery (client.Discover) to find issuer and JWKS URI - remote, auto-refreshing JWKS (rp.NewRemoteKeySet) - signature, expiry and issuer checks (op.NewAccessTokenVerifier, op.VerifyAccessToken) - the verified claim set and role lookup (oauth.IntrospectionContext, IsGrantedRole) What stays local: - a multi-audience allow-list, because this gateway is a shared resource server and the SDK's oauth.WithJWT accepts exactly one client ID - an adapter from IntrospectionContext to jwt.MapClaims so the rest of the gateway keeps one claim representation Other changes: - issuer is configuration-driven; the hardcoded authentik.riotpiao.com check is gone - aud may be a string or an array (ZITADEL sends an array) - CheckPermissions also reads urn:zitadel:iam:org:project:roles, the claim ZITADEL uses for project roles - requiredCapability accepts a comma-separated list, so llm:inference and llm-inference both satisfy the LLM gate - a five-segment token is reported as an encrypted JWE with the fix in the message, instead of failing as a bad signature Config: auth.zitadelDomain turns the SDK path on; jwksUrl stays supported for the legacy path and for tests.
This commit is contained in:
+44
-6
@@ -98,8 +98,29 @@ func New(cfg *config.Config) *Handler {
|
||||
defaultMaxBodySize: 100 * 1024 * 1024,
|
||||
}
|
||||
|
||||
// Initialize JWT validator if auth is enabled
|
||||
if cfg.Auth.Enabled && cfg.Auth.JWKSURL != "" {
|
||||
// Initialize JWT validator if auth is enabled.
|
||||
//
|
||||
// Two paths, in order of preference:
|
||||
// 1. ZitadelDomain set -> official ZITADEL Go SDK (discovery + remote JWKS
|
||||
// + op.AccessTokenVerifier). This is the production path.
|
||||
// 2. JWKSURL set -> legacy raw-JWKS validation, kept for tests and for
|
||||
// any deployment that has not moved to ZITADEL yet.
|
||||
if cfg.Auth.Enabled && cfg.Auth.ZitadelDomain != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
v, err := auth.NewZitadelValidator(ctx, cfg.Auth.Issuer, cfg.Auth.ZitadelClientIDs, auth.ZitadelConfig{
|
||||
Domain: cfg.Auth.ZitadelDomain,
|
||||
InsecurePort: cfg.Auth.ZitadelInsecurePort,
|
||||
ClientIDs: cfg.Auth.ZitadelClientIDs,
|
||||
})
|
||||
if err != nil {
|
||||
logging.Errorf("zitadel: verifier init failed; auth disabled until restart", err, map[string]string{
|
||||
"domain": cfg.Auth.ZitadelDomain,
|
||||
})
|
||||
} else {
|
||||
h.jwtValidator = v
|
||||
}
|
||||
} else if cfg.Auth.Enabled && cfg.Auth.JWKSURL != "" {
|
||||
h.jwtValidator = auth.NewValidator(
|
||||
cfg.Auth.Issuer,
|
||||
cfg.Auth.Audience,
|
||||
@@ -722,16 +743,17 @@ func (h *Handler) authenticateV1(w http.ResponseWriter, r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
identity.Inject(r, claims)
|
||||
if h.config.Auth.RequiredCapability != "" {
|
||||
if !h.jwtValidator.CheckPermissions(claims, h.config.Auth.RequiredCapability, "*") {
|
||||
if caps := requiredCapabilities(h.config.Auth.RequiredCapability); len(caps) > 0 {
|
||||
args := append(caps, "*")
|
||||
if !h.jwtValidator.CheckPermissions(claims, args...) {
|
||||
writeProblemDetail(w, http.StatusForbidden,
|
||||
"https://api.example.com/problems/insufficient-permissions",
|
||||
"Insufficient Permissions",
|
||||
fmt.Sprintf("Required capability: %s", h.config.Auth.RequiredCapability),
|
||||
fmt.Sprintf("Required capability: %s", strings.Join(caps, " or ")),
|
||||
nil)
|
||||
logging.Errorf("auth failed", fmt.Errorf("insufficient permissions"), map[string]string{
|
||||
"path": r.URL.Path,
|
||||
"required": h.config.Auth.RequiredCapability,
|
||||
"required": strings.Join(caps, ","),
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -739,6 +761,22 @@ func (h *Handler) authenticateV1(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// requiredCapabilities splits the comma-separated requiredCapability setting
|
||||
// into a list. Several spellings are accepted because the same capability is
|
||||
// named differently by each identity provider: Authentik used "llm:inference"
|
||||
// as a permission string, while ZITADEL uses role keys such as
|
||||
// "llm-inference". A token satisfies the gate when it carries any one of them.
|
||||
func requiredCapabilities(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// enforceBodyLimit caps request body size for non-chat routes.
|
||||
// Returns false when the request is rejected (response already written).
|
||||
func (h *Handler) enforceBodyLimit(w http.ResponseWriter, r *http.Request, route *Route) bool {
|
||||
|
||||
Reference in New Issue
Block a user