// Package authn validates Keycloak-issued bearer tokens for the // tenant-registry API (RBAC rollout Phase 1; fail-closed per the ratified // compliance auth design, model B2). // // The package only verifies — issuer, signature via JWKS, expiry, and // audience. It deliberately does not authorize: tenant-registry IS the // membership authority, so its callers are services (INTERNAL_SERVICE_ONLY // posture) holding client_credentials tokens whose audience includes // AUTH_EXPECTED_AUDIENCE. package authn import ( "context" "errors" "fmt" "strings" "github.com/coreos/go-oidc/v3/oidc" ) // ErrNoToken means the Authorization header was absent or not a Bearer // scheme. Handlers map it to 401 TOKEN_MISSING. var ErrNoToken = errors.New("authorization header missing or not Bearer") // Principal is the verified caller identity, placed in the request context // so handlers (and, later, audit writes) can attribute actions. type Principal struct { Subject string // JWT sub — the Keycloak user or service-account id ClientID string // JWT azp — which OAuth client obtained the token Issuer string } type ctxKey struct{} // WithPrincipal returns ctx carrying p. func WithPrincipal(ctx context.Context, p *Principal) context.Context { return context.WithValue(ctx, ctxKey{}, p) } // PrincipalFrom extracts the verified principal, if any. func PrincipalFrom(ctx context.Context) (*Principal, bool) { p, ok := ctx.Value(ctxKey{}).(*Principal) return p, ok } // Verifier checks bearer tokens against one issuer + audience. A nil // *Verifier means auth is disabled (AUTH_ENABLED=false) and the server // passes requests through unauthenticated. type Verifier struct { issuer string verifier *oidc.IDTokenVerifier } // New performs OIDC discovery against issuer and prepares JWKS-backed // verification. Callers must treat an error as fatal when AUTH_ENABLED=true // (AUTH_CONFIG_INCOMPLETE — refuse to start, never fall open). func New(ctx context.Context, issuer, audience string) (*Verifier, error) { if issuer == "" || audience == "" { return nil, errors.New("issuer and audience are required") } provider, err := oidc.NewProvider(ctx, issuer) if err != nil { return nil, fmt.Errorf("oidc discovery for %s: %w", issuer, err) } return &Verifier{ issuer: issuer, verifier: provider.Verifier(&oidc.Config{ClientID: audience}), }, nil } // Verify checks the raw Authorization header value and returns the caller // principal. Signature, issuer, expiry, and audience are all enforced by // the underlying oidc verifier. func (v *Verifier) Verify(ctx context.Context, authorization string) (*Principal, error) { raw, ok := strings.CutPrefix(authorization, "Bearer ") if !ok || strings.TrimSpace(raw) == "" { return nil, ErrNoToken } tok, err := v.verifier.Verify(ctx, strings.TrimSpace(raw)) if err != nil { return nil, err } var claims struct { Azp string `json:"azp"` } _ = tok.Claims(&claims) // azp is informational; absence is not an error return &Principal{Subject: tok.Subject, ClientID: claims.Azp, Issuer: tok.Issuer}, nil }