Accept Tessera (Keycloak-compatible) OIDC tokens as API bearer
Adds an additive bearer-verification path: verify RS256 access tokens against Tessera's JWKS (iss/aud/exp), map sub/preferred_username/email + roles (realm_access.roles, resource_access.<audience>.roles) to the app's identity. Existing auth (API keys / app JWTs / sessions) is unchanged. Issuer + audience are env-configurable. Validated end-to-end against the local sim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,7 @@ func Mount(cfg *config.Config, db *sqlx.DB, oidcSvc *oidc.Service, version strin
|
||||
// to do with anonymous (typically: serve public subset, hide private).
|
||||
func optionalAuthChain(db *sqlx.DB, cfg *config.Config, verifier auth.SessionVerifier) func(http.Handler) http.Handler {
|
||||
agent := auth.AgentAPIKey(db, cfg.AgentAPIKeyPepper)
|
||||
tessera := auth.TesseraBearer(cfg.OIDCBearerIssuer, cfg.OIDCBearerAudience)
|
||||
oidcMw := auth.OIDCBrowser(verifier, cfg.IsDev(), cfg.OIDCDevBypassToken, cfg.OIDCOnly)
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -172,6 +173,14 @@ func optionalAuthChain(db *sqlx.DB, cfg *config.Config, verifier auth.SessionVer
|
||||
if rw.status != http.StatusUnauthorized {
|
||||
return
|
||||
}
|
||||
// Not an agent key (opaque) → try a Tessera (external OIDC)
|
||||
// bearer JWT. Opaque agent keys fail JWT parse here and 401,
|
||||
// which we again demote to "fall through".
|
||||
rw = &captureWriter{ResponseWriter: w}
|
||||
tessera(next).ServeHTTP(rw, r)
|
||||
if rw.status != http.StatusUnauthorized {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Try OIDC (session cookie) — always (no header gate needed,
|
||||
// cookie presence is implicit) so an authenticated browser
|
||||
@@ -192,6 +201,7 @@ func optionalAuthChain(db *sqlx.DB, cfg *config.Config, verifier auth.SessionVer
|
||||
// requireAnyAuthChain: 401 if neither agent nor user auth succeeds.
|
||||
func requireAnyAuthChain(db *sqlx.DB, cfg *config.Config, verifier auth.SessionVerifier) func(http.Handler) http.Handler {
|
||||
agent := auth.AgentAPIKey(db, cfg.AgentAPIKeyPepper)
|
||||
tessera := auth.TesseraBearer(cfg.OIDCBearerIssuer, cfg.OIDCBearerAudience)
|
||||
oidcMw := auth.OIDCBrowser(verifier, cfg.IsDev(), cfg.OIDCDevBypassToken, cfg.OIDCOnly)
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -201,6 +211,12 @@ func requireAnyAuthChain(db *sqlx.DB, cfg *config.Config, verifier auth.SessionV
|
||||
if rw.status != http.StatusUnauthorized {
|
||||
return
|
||||
}
|
||||
// Not an agent key → try a Tessera (external OIDC) bearer JWT.
|
||||
rw = &captureWriter{ResponseWriter: w}
|
||||
tessera(next).ServeHTTP(rw, r)
|
||||
if rw.status != http.StatusUnauthorized {
|
||||
return
|
||||
}
|
||||
}
|
||||
oidcMw(next).ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user