auth

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package auth provides OIDC-based authentication primitives: a Validator interface plus a generic OIDCValidator implementation, principal extraction from a verified JWT and chi middleware that runs the validation. IdP-specific validators live in sub-packages and register themselves with the mode registry behind build tags. See `oidc_validator.go` for the generic JWT validation pipeline and `mode.go` for runtime mode selection.

Index

Constants

View Source
const (
	// DiscoveryTimeout bounds the OpenID Provider metadata request the "oidc"
	// mode makes at startup when no JWKS URL is configured.
	DiscoveryTimeout = 10 * time.Second
)
View Source
const MockAudience = "mock-client"

MockAudience is the audience claim expected by mock-mode tokens. The mockoidc fixture's Issue() helper sets `aud = clientID`, so callers minting tokens for mock-mode validation should use this string as the clientID.

Variables

View Source
var (
	// ErrKeysUnavailable reports that no key set could be loaded in time.
	ErrKeysUnavailable = errors.New("jwks: keys not available")
	// ErrKeySourceClosed reports a call on a KeySource after Close.
	ErrKeySourceClosed = errors.New("jwks: key source closed")
)
View Source
var (
	// ErrTypMismatch rejects a token whose typ header is not the required one.
	ErrTypMismatch = errors.New("token typ does not match the required typ")
	// ErrAZPNotAllowed rejects a token whose azp claim is missing or not listed.
	ErrAZPNotAllowed = errors.New("azp is not an allowed authorized party")
)
View Source
var (
	// ErrTokenEmpty rejects an empty bearer token.
	ErrTokenEmpty = errors.New("empty bearer token")
	// ErrTokenMalformed rejects a token that is not a parsable JWS.
	ErrTokenMalformed = errors.New("token is not a well-formed JWS")
	// ErrTokenSignatures rejects a JWS without exactly one signature.
	ErrTokenSignatures = errors.New("token must carry exactly one signature")
	// ErrAlgNotAllowed rejects a header alg outside the asymmetric allow-list.
	ErrAlgNotAllowed = errors.New("token alg not allowed")
	// ErrAlgKeyMismatch rejects a header alg that differs from the key's alg.
	ErrAlgKeyMismatch = errors.New("token alg does not match the key alg")
	// ErrKidMissing rejects a token without a kid header.
	ErrKidMissing = errors.New("token has no kid")
	// ErrNestedToken rejects a payload that is not a JSON claim set.
	ErrNestedToken = errors.New("token payload is not a JSON object")
	// ErrTokenInvalid rejects a token whose signature does not verify against
	// the key set, for example because no usable key matches its kid.
	ErrTokenInvalid = errors.New("token signature verification failed")
	// ErrExpMissing rejects a token without an exp claim.
	ErrExpMissing = errors.New("exp claim missing")
	// ErrTokenExpired rejects a token past its exp.
	ErrTokenExpired = errors.New("token expired")
	// ErrTokenNotYetValid rejects a token before its nbf.
	ErrTokenNotYetValid = errors.New("token not yet valid (nbf)")
	// ErrTokenIssuedAt rejects a token whose iat lies in the future.
	ErrTokenIssuedAt = errors.New("token iat is in the future")
	// ErrTokenClaims is the catch-all for a verified token whose claim set
	// cannot be decoded or fails a validation not named above.
	ErrTokenClaims = errors.New("token claims invalid")
	// ErrSubMissing rejects a token without a non-empty sub claim.
	ErrSubMissing = errors.New("sub claim missing")
	// ErrIssuerMismatch rejects a token whose iss is not the expected issuer.
	ErrIssuerMismatch = errors.New("iss does not match the configured issuer")
	// ErrAudienceMissing rejects a token without an aud claim.
	ErrAudienceMissing = errors.New("aud claim missing")
	// ErrAudienceMismatch rejects a token whose aud names no expected audience.
	ErrAudienceMismatch = errors.New("aud does not contain the expected audience")
)

Token check errors. Their text names the rule, never a claim or header value, so callers may log them. Errors from jwx are mapped onto them and not wrapped, because jwx messages can carry the kid or a URL.

View Source
var ErrModeUnavailable = errors.New("auth mode unavailable in this build")

ErrModeUnavailable indicates the requested KAFKITO_AUTH_MODE is not compiled in. Most notably, "off" is gated behind the `devauth` build tag.

Functions

func AudienceContains

func AudienceContains(tok jwt.Token, want string) bool

AudienceContains reports whether tok's "aud" claim contains want.

func HostBelongsToDomain

func HostBelongsToDomain(rawURL, domain string) bool

HostBelongsToDomain reports whether rawURL's host equals domain or sits one or more labels below it (e.g. "auth.example.com" belongs to "example.com"). The comparison is case-insensitive. Used by IdP-specific validators that constrain JKU URLs to an allow-list domain.

func Middleware

func Middleware(v Validator) func(http.Handler) http.Handler

Middleware returns an http middleware that validates the bearer token via v and stores the resulting Principal in the request context. On any failure it responds with 401 and writes a structured error.

func MiddlewareFor

func MiddlewareFor(v Validator) func(http.Handler) http.Handler

MiddlewareFor returns the correct http.Handler middleware for the given Validator. If v implements syntheticValidator (i.e. the devauth off-mode alwaysAllowValidator) the middleware injects the synthetic Principal on every request without requiring a Bearer header. Otherwise it delegates to Middleware which enforces bearer token validation.

func MiddlewareWithSyntheticPrincipal

func MiddlewareWithSyntheticPrincipal(p *Principal) func(http.Handler) http.Handler

MiddlewareWithSyntheticPrincipal injects p on every request without checking any header. Used by `off` mode so local dev does not need a token at all.

func ParseToken added in v1.3.0

func ParseToken(raw string, set jwk.Set) (jwt.Token, error)

ParseToken verifies raw against set and validates its time claims. raw must be a compact JWS with one signature, a kid and a JSON claim set (no nested token). The header alg must be one of the allowed asymmetric algorithms and, when the key named by kid carries an alg, equal it; a key without alg is used with the algorithm inferred from its type. exp is required, nbf and iat are checked when present (60 s skew), and sub must be a non-empty string. Issuer and audience are left to the caller.

func Register

func Register(name string, factory ModeFactory)

Register binds a ModeFactory to the given mode name. Intended for use from init() in mode-specific files; each build registers every name once, so the result does not depend on init order. Re-registering a name overwrites the previous factory.

func TokString

func TokString(tok jwt.Token, key string) (string, bool)

TokString reads a string-typed claim by key. Returns ok=false if the key is missing or has a non-string value.

func TokStringSlice

func TokStringSlice(tok jwt.Token, key string) []string

TokStringSlice reads a claim that may be either a JSON string array or a space-separated string (the latter being how some OIDC IdPs encode "scope"). Returns nil when the claim is missing or empty.

func WithPrincipal

func WithPrincipal(ctx context.Context, p *Principal) context.Context

WithPrincipal returns a context carrying p.

Types

type KeySource added in v1.3.0

type KeySource struct {
	// contains filtered or unexported fields
}

KeySource loads and caches the JSON Web Key Set served at one URL.

All callers share one fetch at a time, and a new fetch starts at most once per keyRefreshInterval: to load the first set, when a token names a kid the cached set lacks, or in the background once the set is older than keyMaxAge. A caller waits at most keyWaitTimeout (or until its context ends) and then fails instead of hanging. Close stops a running fetch. The zero value is not usable; call NewKeySource.

func NewKeySource added in v1.3.0

func NewKeySource(url string, opts ...KeySourceOption) *KeySource

NewKeySource returns a KeySource for the JWKS at url. It does no I/O; the first Keys call (or a validator's WarmUp) loads the set.

func (*KeySource) Close added in v1.3.0

func (s *KeySource) Close()

Close cancels a running fetch, waits for it to end and makes later Keys calls fail with ErrKeySourceClosed. Calling Close more than once is safe.

func (*KeySource) Keys added in v1.3.0

func (s *KeySource) Keys(ctx context.Context, kid string) (jwk.Set, error)

Keys returns the key set to verify a token whose header names kid.

If the cached set holds kid (or kid is empty) it is returned at once; a stale set additionally starts a background refresh. Otherwise Keys starts a fetch, unless one is running or the refresh interval has not passed, and waits for the running fetch, bounded by ctx and keyWaitTimeout. It then returns the newest set even if that still lacks kid, so signature verification decides. Without any set it returns ErrKeysUnavailable.

type KeySourceOption added in v1.3.0

type KeySourceOption func(*KeySource)

KeySourceOption configures a KeySource.

func WithURLHiddenInLogs added in v1.3.0

func WithURLHiddenInLogs() KeySourceOption

WithURLHiddenInLogs keeps the URL, and error text that may carry it, out of the log line for a failed fetch. Use it when the URL comes from a token header rather than from configuration.

type MockOIDC

type MockOIDC struct {
	Server *httptest.Server
	// contains filtered or unexported fields
}

MockOIDC is an in-process JWKS+token issuer used in tests. It signs RS256 tokens with a freshly generated key, exposes the public key at /jwks and OpenID Provider metadata at /.well-known/openid-configuration (issuer = Server.URL), and lets callers mint tokens with arbitrary claims via Issue() or the Token builder.

By default the mock emits a generic OIDC token: scopes are written to the "scope" claim verbatim, and no tenant/zone claim is added. Callers that need tenant-flavored tokens (e.g. SAP-style scope namespacing or a zone claim) can opt in via MockOIDCOption values passed to NewMockOIDC.

func NewMockOIDC

func NewMockOIDC(opts ...MockOIDCOption) (*MockOIDC, error)

NewMockOIDC starts the mock and returns it. By default tokens carry no zone claim and scopes are not namespaced; use WithScopePrefix / WithZoneID to opt into tenant-flavored behavior.

func (*MockOIDC) Close

func (m *MockOIDC) Close()

Close stops the test server.

func (*MockOIDC) Host

func (m *MockOIDC) Host() string

Host returns the bare hostname (no port) of the mock server, suitable for use as a UAADomain in tests.

func (*MockOIDC) Issue

func (m *MockOIDC) Issue(sub, clientID, issuer string, scopes []string, extra map[string]any) (string, error)

Issue mints a signed RS256 JWT with sensible defaults plus caller-supplied claims. Pass scopes as local names; if the mock was constructed with WithScopePrefix, each scope is namespaced as prefix + "." + scope. It is shorthand for Token(sub, clientID, issuer).Scopes(scopes...).Claims(extra).Sign().

func (*MockOIDC) IssueWithJKU added in v1.3.0

func (m *MockOIDC) IssueWithJKU(jku, sub, clientID, issuer string, scopes []string) (string, error)

IssueWithJKU mints a token like Issue (without extra claims) but puts jku into the jku header, for testing the validator's jku policy.

func (*MockOIDC) IssueWithoutJKU

func (m *MockOIDC) IssueWithoutJKU(sub, clientID, issuer string, scopes []string) (string, error)

IssueWithoutJKU mints a token like Issue but deliberately omits the jku header, for testing the validator's "missing jku" rejection branch.

func (*MockOIDC) JKU

func (m *MockOIDC) JKU() string

JKU returns the JWKS URL the mock serves.

func (*MockOIDC) JWKSRequests added in v1.3.0

func (m *MockOIDC) JWKSRequests() int64

JWKSRequests returns how many requests the JWKS endpoint has answered.

func (*MockOIDC) RotateKey added in v1.3.0

func (m *MockOIDC) RotateKey() error

RotateKey replaces the signing key with a new RSA key under a new kid ("test-key-1", "test-key-2", ...). The JWKS then serves only the new key, like an IdP that rotated and dropped the old one; tokens issued afterwards are signed with it. NewMockOIDC calls it once for the first key.

func (*MockOIDC) Token added in v1.3.0

func (m *MockOIDC) Token(sub, clientID, issuer string) *TokenBuilder

Token starts a token with the default claims: sub, iss, aud = [clientID], iat, exp (now + 30 min), cid, scope, and zid when the mock has a zone. The token is signed RS256 with the current key, has typ "JWT" and carries the mock's jku.

type MockOIDCOption

type MockOIDCOption func(*MockOIDC)

MockOIDCOption configures a MockOIDC at construction time.

func WithJWKSPath added in v1.3.0

func WithJWKSPath(path string) MockOIDCOption

WithJWKSPath serves the key set at path instead of "/jwks". XSUAA tests use "/token_keys", the only path the xsuaa validator accepts in a jku.

func WithScopePrefix

func WithScopePrefix(prefix string) MockOIDCOption

WithScopePrefix namespaces every scope passed to Issue/IssueWithoutJKU as prefix + "." + scope. An empty prefix disables namespacing (the default).

func WithZoneID

func WithZoneID(zoneID string) MockOIDCOption

WithZoneID makes the mock emit a "zid" claim on every issued token. An empty zoneID disables emission (the default).

func WithoutJWKAlg added in v1.3.0

func WithoutJWKAlg() MockOIDCOption

WithoutJWKAlg serves the public key without an "alg" member, like IdPs (Microsoft Entra ID, for example) that publish keys by type only.

type ModeConfig

type ModeConfig struct {
	// Mode selects which registered factory to use (e.g. "off", "mock"). The
	// set of valid values depends on the build tags the binary was compiled
	// with: every build registers "off", "mock" and "oidc"; tagged builds may
	// register additional IdP-specific modes.
	Mode string
	// VCAPServices is the raw VCAP_SERVICES JSON, used by IdP modes that
	// expect their credentials in a Cloud Foundry service binding.
	VCAPServices string
	// OIDC configures the generic "oidc" mode. An empty JWKSEndpoint triggers
	// OpenID Connect discovery at startup.
	OIDC OIDCConfig
}

ModeConfig drives validator construction. Fields not relevant to a given mode are simply ignored by that mode's factory.

type ModeFactory

type ModeFactory func(cfg ModeConfig) (Validator, func(), error)

ModeFactory constructs a Validator for a registered auth mode. The returned cleanup function may be nil when the mode has nothing to release; BuildValidator replaces it with a no-op so callers can always defer it.

type OIDCConfig

type OIDCConfig struct {
	// IssuerURL is the expected "iss" claim. Tokens whose iss is not exactly
	// this string are rejected; nothing is normalized, so a trailing slash
	// counts.
	IssuerURL string
	// Audience is the expected "aud" claim. Tokens whose aud claim does not
	// contain this value are rejected.
	Audience string
	// JWKSEndpoint is the URL serving the issuer's JSON Web Key Set used to
	// verify token signatures. Required by NewOIDCValidator, which does not
	// auto-discover; the "oidc" mode factory resolves it via OpenID Connect
	// discovery when unset.
	JWKSEndpoint string
	// RequiredTyp, when set, is the "typ" header every token must carry, for
	// example "at+jwt" (RFC 9068). It is compared case-insensitively and an
	// "application/" prefix is ignored on either side (RFC 7515 section
	// 4.1.9). Empty disables the check.
	RequiredTyp string
	// AllowedAZP, when non-empty, lists the "azp" (authorized party) values a
	// token may carry, compared exactly; a token without azp is rejected.
	// Empty disables the check.
	AllowedAZP []string
}

OIDCConfig configures a generic OIDC JWT validator.

type OIDCValidator

type OIDCValidator struct {
	// contains filtered or unexported fields
}

OIDCValidator validates asymmetrically signed JWTs against a fixed issuer/audience and a JWKS endpoint. It backs both the "mock" and the generic "oidc" modes and works with any OIDC IdP that publishes a JWKS URL.

func NewOIDCValidator

func NewOIDCValidator(cfg OIDCConfig) (*OIDCValidator, error)

NewOIDCValidator constructs a validator. It does no I/O: the key set is loaded by WarmUp or the first Validate call, refreshed at most once per minute when a token names an unknown kid, and a request waits at most 5 s for it (see KeySource). Call Close to release the validator.

func (*OIDCValidator) Close added in v1.3.0

func (o *OIDCValidator) Close()

Close releases the key source. Validate fails afterwards.

func (*OIDCValidator) Config added in v1.3.0

func (o *OIDCValidator) Config() OIDCConfig

Config returns the settings the validator enforces.

func (*OIDCValidator) Validate

func (o *OIDCValidator) Validate(ctx context.Context, raw string) (*Principal, error)

Validate parses and signature-verifies raw (see ParseToken for the algorithm and exp/nbf/sub rules), then enforces iss, aud and, when configured, typ and azp.

func (*OIDCValidator) WarmUp added in v1.3.0

func (o *OIDCValidator) WarmUp(ctx context.Context) error

WarmUp loads the key set once, bounded by ctx and the key wait timeout. Mode factories call it at startup and only log a failure; Validate retries the load once the refresh interval has passed.

type Principal

type Principal struct {
	Subject    string   // sub
	Email      string   // email (may be empty depending on IdP)
	UserName   string   // user_name (or preferred_username)
	GivenName  string   // given_name
	FamilyName string   // family_name
	Origin     string   // origin (IdP key)
	Tenant     string   // tenant identifier (claim shape varies per IdP)
	Scopes     []string // local scope names (IdP-specific prefixes stripped)
}

Principal is the authenticated caller as derived from a verified OIDC token (or a synthetic value injected by the dev/mock modes).

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (*Principal, bool)

PrincipalFromContext extracts the principal previously stored via WithPrincipal.

func (*Principal) HasScope

func (p *Principal) HasScope(name string) bool

HasScope reports whether the principal carries the given local scope name. A nil principal has no scopes.

type TokenBuilder added in v1.3.0

type TokenBuilder struct {
	// contains filtered or unexported fields
}

TokenBuilder mints one token. Start it with MockOIDC.Token, chain the setters, and finish with Sign. Setters return the builder; a builder is not safe for concurrent use.

func (*TokenBuilder) Alg added in v1.3.0

Alg signs with alg instead of RS256 and names it in the header. RS* and PS* use the mock's RSA key. HS* uses the DER encoding of the mock's public RSA key as the HMAC secret, so tests can check that a verifier does not treat the public key as a shared secret.

func (*TokenBuilder) Claim added in v1.3.0

func (b *TokenBuilder) Claim(name string, value any) *TokenBuilder

Claim sets or overrides one claim.

func (*TokenBuilder) Claims added in v1.3.0

func (b *TokenBuilder) Claims(claims map[string]any) *TokenBuilder

Claims sets or overrides every claim in claims.

func (*TokenBuilder) JKU added in v1.3.0

func (b *TokenBuilder) JKU(jku string) *TokenBuilder

JKU sets the jku header; "" leaves the header out.

func (*TokenBuilder) Scopes added in v1.3.0

func (b *TokenBuilder) Scopes(scopes ...string) *TokenBuilder

Scopes sets the scope claim (namespaced like Issue).

func (*TokenBuilder) Sign added in v1.3.0

func (b *TokenBuilder) Sign() (string, error)

Sign builds and signs the token.

func (*TokenBuilder) Type added in v1.3.0

func (b *TokenBuilder) Type(typ string) *TokenBuilder

Type sets the typ header; "" leaves the header out.

func (*TokenBuilder) Without added in v1.3.0

func (b *TokenBuilder) Without(names ...string) *TokenBuilder

Without removes the named claims, defaults included, from the token.

func (*TokenBuilder) WithoutKeyID added in v1.3.0

func (b *TokenBuilder) WithoutKeyID() *TokenBuilder

WithoutKeyID leaves the kid header out.

type Validator

type Validator interface {
	Validate(ctx context.Context, rawToken string) (*Principal, error)
}

Validator authenticates a raw bearer token and returns a Principal.

func BuildValidator

func BuildValidator(cfg ModeConfig) (Validator, func(), error)

BuildValidator returns the configured validator plus a cleanup function callers should defer (mock mode uses it to stop the embedded server). On success the cleanup is never nil.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL