auth

package
v0.66.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package auth verifies bearer credentials (RFC 9068 style JWTs) and turns a verified credential into a Principal carried on the request context.

The package performs identification, not authorization. A verified Principal states who the caller claims to be according to a trusted issuer; it grants nothing. Handlers remain responsible for deciding whether that identity may perform the requested operation — the middleware never short-circuits a request on authorization grounds, only on a missing or invalid credential.

Verification is RSA-only: RS256 and PS256 are the sole accepted signature algorithms, and a key set entry that is not an RSA public key is not usable. Symmetric algorithms are deliberately unsupported, so a key-confusion downgrade to HS256 has no code path to reach.

There are two doors onto a Verifier. NewVerifier fetches the issuer's key set from its JWKS endpoint, refreshes it in the background and on an unknown kid, and owns what it built — its Close stops that refresh. NewVerifierWithResolver takes a PublicKeyResolver the caller pins out of band and closes nothing. A key set that cannot be refreshed is served until it passes its configured stale ceiling and then reports ErrKeySetUnavailable: there is no path on which an unverifiable credential is accepted.

Nothing in this package logs, records, or renders the credential itself or the "sub" claim. Verification failures are reported by class (see VerificationError), which is safe to log at DEBUG; the credential string, signature bytes and subject never appear in an error message.

VerificationError.Cause is diagnostic detail for framework DEBUG logging. Its contents come from whichever library rejected the credential, so they are not part of this package's compatibility promise and may change without notice. Callers must not render a Cause into a response body: log the Class instead, which is the only failure detail the package guarantees is safe to expose.

A Principal must likewise not be rendered field by field, and it closes that hole itself: String, Format and MarshalJSON all derive from one redacted shape that keeps the issuer, audience and expiry and replaces the subject and every claim value with an elision marker. Implementing fmt.Formatter takes precedence over fmt.Stringer for every verb, so %v, %s, %q, %+v and %#v are all elided — for both Principal and *Principal — and MarshalJSON covers json.Marshal and the encoders built on it.

One path bypasses all of that: the framework logger's reflective filter (logger.LogEventAdapter.Interface → SensitiveDataFilter.FilterValue → filterStructWithProtection) rebuilds a struct into a map[string]any by reflection, reading exported fields directly before any marshaler or formatter runs. No method on Principal can influence it, and the filter matches field NAMES, so neither "Subject" nor an issuer-chosen claim key is masked. Do not hand a Principal to logger.Interface or to WithFields.

Index

Constants

View Source
const (
	AlgRS256 = config.AlgorithmRS256
	AlgPS256 = config.AlgorithmPS256
)

Accepted signature algorithms. The set is closed: RSA-only, and matched case-sensitively so a misspelled operator value fails startup instead of silently widening the allowlist. They are the config package's spellings, so the allowlist config load enforces is the one the verifier enforces.

Variables

View Source
var (
	// ErrMissingCredential reports that the request presented no bearer credential.
	ErrMissingCredential = errors.New("auth: missing bearer credential")

	// ErrInvalidCredential is the umbrella for every verification-rule failure.
	// Callers match on it with errors.Is and map it to 401.
	ErrInvalidCredential = errors.New("auth: invalid credential")

	// ErrKeySetUnavailable reports that the issuer key set was never fetched or
	// is past its stale ceiling, so no verification decision can be made.
	ErrKeySetUnavailable = errors.New("auth: issuer key set unavailable")

	// ErrKidUnknown reports that the credential's kid is absent from the key set.
	// A PublicKeyResolver returns it; the verifier folds it into the invalid-credential class.
	ErrKidUnknown = errors.New("auth: kid not present in key set")
)

Sentinel errors returned by credential verification. ErrKeySetUnavailable is deliberately outside the ErrInvalidCredential chain: an unreachable issuer key set is a server-side fault (503), while every other failure below is a caller fault (401).

Functions

func ContextWithPrincipal

func ContextWithPrincipal(ctx context.Context, p Principal) context.Context

ContextWithPrincipal returns a copy of ctx carrying p, symmetric with PrincipalFromContext.

Verify deliberately does not call this: it is transport-neutral and returns the Principal instead, so the caller — this package's HTTP middleware, or a gRPC interceptor living outside it — decides where on the context chain the identity is published.

Attaching a Principal asserts identification, not authorization. It states that a credential verified against the configured issuer; whether that identity may perform the operation remains the handler's decision.

Audience is cloned on the way in and on the way out, so no reader shares a backing array with the caller or with another reader: a Principal is read concurrently by every handler on the request, and a slice header copy would let one of them change what the others see the credential was issued for. Claims stays shared under the read-only contract documented on the field — copying it would allocate per request and still not protect the nested values that JSON decoding produces.

func Middleware

func Middleware(v *Verifier) server.MiddlewareFunc

Middleware returns the HTTP middleware that verifies the request's bearer credential and attaches the resulting Principal to the request context, where PrincipalFromContext reads it back.

It is attached PER ROUTE GROUP — RouteRegistrar.Group(prefix, auth.Middleware(v)) or Use — never globally. There is deliberately no path allowlist and no GlobalMiddlewareRegisterer path: a route that must stay open (a probe, a webhook with its own signature check) is exempted by not attaching the middleware to its group, which keeps the exemption visible at the registration site instead of buried in a skip list.

It performs identification, not authorization. A request that reaches the handler carries a Principal whose credential verified against the configured issuer; whether that identity may perform the operation stays the handler's decision. Nothing here inspects claims or cross-checks the tenant.

Outcomes:

  • No Authorization header, a non-Bearer scheme, or an empty token — 401 with WWW-Authenticate: Bearer realm="<issuer>".
  • A credential that failed any verification rule — 401 with WWW-Authenticate: Bearer error="invalid_token".
  • An unusable issuer key set — 503 with Retry-After. It is a server-side fault, deliberately distinct from the 401s: the caller's credential was never judged.

Every rejection returns a server.IAPIError, so the framework's error handler renders the standard envelope, and no rejection calls next: a verification failure cannot fall through to the handler.

SECURITY: nothing here logs, renders or records the credential or the "sub" claim. A rejection is logged at DEBUG by class only, and the response body carries a fixed message. The single exception is the enduser.id span attribute, which records Principal.Subject and is off unless auth.jwt.telemetry.enduserid is true.

It panics on a nil Verifier: the middleware is built during module Init, so a missing verifier is a wiring error that must abort startup rather than fail every request at runtime.

Types

type Class

type Class string

Class names the rule that rejected a credential. It is the only failure detail that may be logged or rendered: it identifies the rule without carrying any part of the credential itself.

Class is a telemetry dimension — a log field today, a metric attribute tomorrow — and never a control-flow discriminator. The 401/503 split is carried entirely by the ErrInvalidCredential / ErrKeySetUnavailable sentinel chain, so callers branch with errors.Is and read a Class only to report.

const (
	ClassMalformed      Class = "malformed"
	ClassAlgorithm      Class = "algorithm"
	ClassKidMissing     Class = "kid_missing"
	ClassKidUnknown     Class = "kid_unknown"
	ClassSignature      Class = "signature"
	ClassIssuer         Class = "issuer"
	ClassAudience       Class = "audience"
	ClassExpired        Class = "expired"
	ClassNotYetValid    Class = "not_yet_valid"
	ClassIssuedInFuture Class = "issued_in_future"
	ClassMissingExpiry  Class = "missing_expiry"
	ClassType           Class = "type"
)

Verification failure classes, each naming one rule inside Verify.

const ClassKeySetUnavailable Class = "key_set_unavailable"

ClassKeySetUnavailable labels the key-set failure in logs and metrics. It is deliberately NOT a VerificationError class: an unusable key set is a server fault (503) reported through ErrKeySetUnavailable, not an invalid credential, so it never reaches VerificationError.Class. It lives in the Class vocabulary only so a log consumer filtering on these constants can find it.

type Config

type Config config.AuthJWTConfig

Config is the bearer-credential verification configuration: the framework's auth.jwt section, taken as a package-local type so the verifier's validation and its typed *ConfigError live next to the code that enforces them.

A module converts the framework's section at its seam:

cfg := auth.Config(deps.Config.Auth.JWT)

config.Validate has already rejected what is wrong for every deployment (see config.checkAuth); Validate below adds what is true only once a verifier is actually built.

func (*Config) Validate

func (c *Config) Validate() error

Validate performs fail-fast validation of the verifier's configuration: issuer, audience, algorithms, leeway and typ. It deliberately leaves the auth.jwt.jwks.* group alone — a verifier built over a pinned PublicKeyResolver fetches nothing — so the JWKS-backed resolver validates that group itself. Every failure is a *ConfigError naming the offending auth.jwt.* key.

type ConfigError

type ConfigError struct {
	Field   string // Section-qualified configuration key that failed validation
	Message string // Human-readable error message
	Err     error  // Underlying error, if any
}

ConfigError represents a configuration error during auth initialization. These errors are fail-fast and should abort application startup.

func NewConfigError

func NewConfigError(field, message string, err error) *ConfigError

NewConfigError creates a new configuration error.

func (*ConfigError) Error

func (e *ConfigError) Error() string

Error implements the error interface.

func (*ConfigError) Unwrap

func (e *ConfigError) Unwrap() error

Unwrap returns the underlying error for errors.Is/As support.

type Principal

type Principal struct {
	// Subject is the "sub" claim. It MUST NOT be logged or recorded.
	Subject string

	// Issuer is the "iss" claim, already matched against the configured issuer.
	Issuer string

	// Audience is the "aud" claim, already matched against the configured audience.
	Audience []string

	// ExpiresAt is the "exp" claim. A credential without one never verifies.
	ExpiresAt time.Time

	// IssuedAt is the "iat" claim; the zero time when the claim was absent.
	IssuedAt time.Time

	// Claims is the raw decoded payload.
	//
	// Aliasing contract: Claims is READ-ONLY. The map is never copied on the way
	// in or out, so every reader of this request's context holds the same map: a
	// delete(p.Claims, …) or a write in one handler, middleware or helper
	// corrupts what every other reader sees. When one of those readers is a
	// background goroutine on a context.WithoutCancel(ctx) — the framework's own
	// pattern for work that outlives the request — the breach is a data RACE, not
	// merely a logic bug. Copy any value out before modifying it.
	//
	// No defensive copy is made on purpose. json.Unmarshal yields nested []any
	// and map[string]any for precisely the claims most likely to be mutated
	// ("scope", "realm_access.roles"), so a shallow copy would cost an allocation
	// on every request while advertising a safety it does not provide for those
	// nested values.
	Claims map[string]any
}

Principal is the identity a verified credential asserts. It is attached to the request context by the auth middleware and read back by handlers.

A Principal is identification, not authorization: its presence means the credential verified against the configured issuer, nothing more.

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (p Principal, ok bool)

PrincipalFromContext returns the identity the auth middleware verified for this request, and whether one was attached. Absence (ok == false) means the request skipped the middleware or carried no credential.

func (Principal) Claim

func (p Principal) Claim(name string) (value any, ok bool)

Claim returns the raw claim stored under name, and whether it was present. A present claim whose value is null returns (nil, true).

func (Principal) Format

func (p Principal) Format(f fmt.State, verb rune)

Format routes every fmt verb through the elided rendering, for both Principal and *Principal.

SECURITY: it exists for %#v, which prints the Go-syntax representation and bypasses fmt.Stringer — the one fmt path that would otherwise dump Subject and Claims. Implementing fmt.Formatter takes precedence over fmt.Stringer for EVERY verb, so %v, %s and %q are answered here too and render exactly what String does; an unsupported verb reports the bad verb with the same elided body rather than falling back to a field dump.

func (Principal) MarshalJSON

func (p Principal) MarshalJSON() ([]byte, error)

MarshalJSON emits the elided rendering rather than the struct's fields.

SECURITY: json.Marshal walks exported fields, so without this a Principal reaching an encoder — an error payload, an audit record, a response body — would serialize Subject and every claim. The emitted object mirrors String.

It does NOT cover the framework logger's reflective filter path (logger.LogEventAdapter.Interface and Logger.WithFields → SensitiveDataFilter), which rebuilds a struct into a map[string]any by reflection before any marshaler runs; no method on Principal can influence that. Do not hand a Principal to it.

func (Principal) String

func (p Principal) String() string

String renders the Principal without its subject or any claim value.

SECURITY: this is the elision seam. A Principal travels on the request context, so it lands in a downstream log.Info().Interface("principal", p) or fmt.Errorf("%v", p) by accident; the logger's SensitiveDataFilter matches field NAMES and cannot help, because "Subject" is not a sensitive name and a claim key is attacker-chosen. Rendering therefore drops Subject and Claims and keeps only the already-public issuer, audience and expiry.

type PublicKeyResolver

type PublicKeyResolver interface {
	PublicKey(ctx context.Context, kid string) (*rsa.PublicKey, error)
}

PublicKeyResolver resolves the issuer's public signing keys by "kid".

It mirrors jose.KeyResolver, the house shape for a key-lookup interface, with one deliberate difference: it is public-key-only. A JWKS-backed resolver must never be able to satisfy a private-key interface, so this is not jose.KeyResolver and never grows a PrivateKey method.

Implementations return ErrKidUnknown when the kid is absent from an otherwise usable key set, and ErrKeySetUnavailable when no usable key set exists at all (never fetched, or past its stale ceiling).

Aliasing contract: the returned key is READ-ONLY, and the same aliasing rule Principal.Claims carries. A resolver resolves keys on the per-request verification path, so it hands back its own key rather than deep-copying a modulus per call; a caller that writes to the returned key — or to its N — corrupts verification for every concurrent request. Copy before modifying.

type StaticKeyResolver

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

StaticKeyResolver is an in-memory PublicKeyResolver over a fixed set of keys. It suits tests and consumers that pin issuer keys out of band instead of fetching JWKS.

The key map and the keys in it are copied at construction and never written afterwards, so a StaticKeyResolver is safe for concurrent use.

func NewStaticKeyResolver

func NewStaticKeyResolver(keys map[string]*rsa.PublicKey) *StaticKeyResolver

NewStaticKeyResolver returns a PublicKeyResolver over a defensive copy of keys. Entries whose key is nil, or whose key is not structurally usable, are dropped: such a key cannot verify anything, so it reads as an unknown kid rather than reaching the verifier, where its failure would be reported as a bad signature on the caller's credential instead of an unusable key set. A resolver that ends up with no keys at all reports ErrKeySetUnavailable on every lookup.

A key is structurally usable when its modulus is non-nil, positive, odd (an RSA modulus is a product of two odd primes) and between 2048 and 16384 bits inclusive, and its public exponent is odd and between 3 and 1<<31-1 inclusive. The 2048-bit floor holds on this path too: a pinned key that a JWKS-backed resolver would refuse must not verify tokens merely because it was configured in code.

Each key is cloned, modulus included, so the resolver owns its key material: a caller that later writes to the keys it passed in cannot retroactively change what this resolver verifies against. The clone is paid once, at construction.

func (*StaticKeyResolver) PublicKey

func (s *StaticKeyResolver) PublicKey(_ context.Context, kid string) (*rsa.PublicKey, error)

PublicKey implements PublicKeyResolver.

The returned key is the resolver's own and MUST NOT be mutated: it is shared by every concurrent lookup of the same kid. See the PublicKeyResolver aliasing contract.

The method is exported, so it can be reached without passing through NewVerifierWithResolver. A nil receiver holds no key set at all, which is exactly the ErrKeySetUnavailable condition — a server-side misconfiguration, never the 401 that ErrKidUnknown would read as.

type VerificationError

type VerificationError struct {
	// Class is one of the Class* constants.
	Class Class

	// Cause is the underlying failure, for framework DEBUG logging only. It is
	// deliberately absent from the errors.Is chain so no cause can reclassify
	// the 401 that this error represents.
	Cause error
}

VerificationError reports which verification rule rejected a credential.

SECURITY: neither Error() nor any field may carry the credential string, the raw token, signature bytes or the "sub" claim. Error() renders the class and nothing else — in particular it never renders Cause, because a library cause routinely embeds the token it failed on, and Format extends that guarantee to every fmt verb, %#v included. Cause is kept for DEBUG-level inspection by the framework only; callers constructing a VerificationError must not pass a cause that embeds the credential.

func NewVerificationError

func NewVerificationError(class Class, cause error) *VerificationError

NewVerificationError builds a VerificationError for the given class. Cause may be nil.

func (VerificationError) Error

func (e VerificationError) Error() string

Error renders the failure class only.

func (VerificationError) Format

func (e VerificationError) Format(f fmt.State, verb rune)

Format routes every fmt verb through the class-only rendering of Error.

SECURITY: it exists for %#v, which prints the Go-syntax representation and bypasses the error interface — the one fmt path that would otherwise dump the exported Cause field and whatever exported fields the cause's own type carries. Implementing fmt.Formatter takes precedence over Error for EVERY verb, so %v and %s are answered here too and render exactly what Error returns; an unsupported verb reports the bad verb with the same safe body rather than falling back to a field dump. It changes no errors.Is/As behavior: fmt rendering and the unwrap chain are separate seams.

The receiver is a VALUE, as it is on Error and Unwrap: a pointer receiver would leave a VerificationError value dumping Cause under %#v, which is the hole this method exists to close. A pointer's method set includes value methods, so *VerificationError is covered by the same guarantee.

func (VerificationError) Unwrap

func (e VerificationError) Unwrap() error

Unwrap returns ErrInvalidCredential so errors.Is(err, ErrInvalidCredential) holds for every verification failure, whatever its class.

type Verifier

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

Verifier verifies compact JWS bearer credentials against a PublicKeyResolver and the configured issuer, audience, algorithm allowlist and clock leeway.

A Verifier is safe for concurrent use: it is immutable after construction and delegates key lookup to the PublicKeyResolver, which carries its own concurrency contract.

SECURITY: no method logs, renders or records the credential string, the signature bytes or the "sub" claim. A rejection is reported by class only.

func NewVerifier

func NewVerifier(cfg Config, log logger.Logger, mp metric.MeterProvider, client httpclient.Client) (*Verifier, error)

NewVerifier builds a verifier over the issuer's JWKS endpoint. It is the consumer-facing door: the pinned-key NewVerifierWithResolver is for deployments that carry issuer keys out of band.

The key set is fetched before this returns and a failed fetch is an error, so a module Init aborts startup rather than booting a verifier that can verify nothing. cfg is validated in full first — the auth.jwt.* rules Config.Validate owns plus the auth.jwt.jwks.* group, which only a fetching resolver makes live.

mp may be nil, in which case the global MeterProvider is used. client may be nil, in which case a default httpclient is built with a peer name derived from the key set endpoint's host; building that default needs a logger, so a nil log and a nil client together are a configuration error.

Ownership: the returned verifier CONSTRUCTED its resolver, so its Close stops the background refresh. Call it from the module's Shutdown.

func NewVerifierWithResolver

func NewVerifierWithResolver(cfg Config, log logger.Logger, resolver PublicKeyResolver) (*Verifier, error)

NewVerifierWithResolver builds a verifier over an explicitly supplied PublicKeyResolver, for consumers that pin issuer keys out of band rather than fetching JWKS.

cfg is validated up front and its *ConfigError is returned unchanged, so a misconfigured service fails startup instead of booting with a widened allowlist. A nil resolver is rejected for the same reason, and so is a non-nil interface holding a nil pointer.

Ownership: resolver stays the CALLER's. The returned verifier never closes it, so a resolver with resources of its own must be shut down by whoever built it.

A nil log is tolerated: the DEBUG rejection logging simply no-ops, which keeps a verifier constructible in a test without wiring a logger. Verification behavior is identical either way. A non-nil interface holding a nil pointer is normalized to the same no-op rather than rejected, because it means the same thing and would otherwise panic on the first rejection.

cfg is taken by value, which copies only the HEADERS of its slice fields, so the stored configuration clones Audience, Algorithms and Typ. Without that, a caller writing to the slice it passed in would change what a live verifier accepts — and race a concurrent Verify — contradicting the type's immutability contract.

func (*Verifier) Close

func (v *Verifier) Close() error

Close releases the resources the verifier itself CONSTRUCTED, and only those. A PublicKeyResolver handed in through NewVerifierWithResolver belongs to the caller and is never closed here, so Close on such a verifier is a no-op; a verifier built by NewVerifier owns the JWKS resolver it constructed and stops its background refresh here.

It is idempotent and always returns nil, so consumers can call it unconditionally from a module Shutdown. It blocks until the refresh goroutine has exited, after which the verifier issues no further requests to the issuer — it keeps verifying against the key set it last held until that set passes its stale ceiling.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, credential string) (Principal, error)

Verify checks credential end to end and returns the identity it asserts.

An empty or whitespace-only credential returns ErrMissingCredential, which is deliberately outside the ErrInvalidCredential chain so a caller can answer "no credential presented" differently from "credential rejected". Every rule failure returns a *VerificationError, for which errors.Is(err, ErrInvalidCredential) holds. An unusable key set returns ErrKeySetUnavailable, which is neither.

The returned Principal's Subject is empty when the credential carries no "sub" claim: the claim is not required by RFC 7519, and a credential may assert an audience-scoped identity without one.

Exactly one auth.verification.total observation is recorded per call, labeled with the outcome. The counter is the only thing this wrapper adds: every verification rule lives in verify below, so the recording point cannot drift away from the decision it reports.

Directories

Path Synopsis
Package testing provides an in-memory fake JWT issuer for exercising the auth package's bearer/JWT verifier without a real identity provider.
Package testing provides an in-memory fake JWT issuer for exercising the auth package's bearer/JWT verifier without a real identity provider.

Jump to

Keyboard shortcuts

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