Security model: authn vs authz¶
authn is deliberately small, and its shape encodes a few security decisions that are
easy to get wrong when auth is hand-rolled per service. This page explains the why
behind the API so you can use it correctly — and extend it without undermining it.
Authentication is not authorization¶
The package draws a hard line between two questions:
- Authentication (who are you?) — a
Verifierturns a credential into a verifiedIdentity. - Authorization (what may you do?) — an
AuthorizeFuncpredicate turns anIdentity(plus request metadata) into an allow/deny decision.
They map to different wire statuses (401 vs 403), fail for different reasons,
and change on different schedules — conflating them is how "authenticated" quietly
becomes "authorized." authn keeps them as separate types so a call site must make
both decisions explicitly.
authn ships no policy engine. Authorization is your predicate — the built-in
RequireScopes/RequireClaim combinators, or your own function. Keeping policy in the
application, expressed as code, avoids a second configuration language to secure and
audit.
Fail closed, and leak nothing¶
Two rules govern every verifier:
- Fail closed. Any condition that is not a clean success is a failure. An empty mTLS chain, an expired or wrong-audience JWT, an unknown API key, a JWKS that will not fetch — all return an error, none return a partial or "probably fine" identity.
- Leak nothing. A
Verifiermust not encode why authentication failed in a user-facing form. The returned error is server-side detail for your log (redacted); the transport maps every verify error to a single generic401 / Unauthenticated. Telling an attacker whether a key exists, a token expired, or an audience mismatched hands them an oracle.
The ErrUnauthenticated
sentinel exists for your benefit — distinguishing a rejected credential (expected,
log quietly) from an operational failure (a network/config problem, log loudly) — and
never crosses the wire.
JWT-specific hardening¶
The JWT verifier bakes in the mitigations for the classic token-verification pitfalls:
- No
alg: none, no algorithm confusion.AllowedAlgorithmsdefaults to the asymmetric RS/ES 256/384/512 set. Bothnoneand everyHS*algorithm are refused at construction, not at verification — there is no configuration in which the verifier could accept an unsigned token, or be tricked into using a JWKS public key as an HMAC secret. Making the two modes mutually exclusive is the only reliable defence against algorithm confusion, which is why there is no override for it. - Standard claims, with two of them mandatory.
expandissare required: a token carrying neither is rejected outright, so a token that never expires cannot be minted by omitting the claim.nbfis checked when present.audmust be one of the configured audiences — leavingAudiencesempty disables that check entirely, which means accepting tokens minted for a different service by the same issuer.expandnbfboth allow a configurableLeeway(default 60s) for clock skew.iatis not validated at all. - Keys from a trusted JWKS over HTTPS. Signing keys are fetched from the configured
(or OIDC-discovered) JWKS endpoint, which must be HTTPS, and held in a cache that is
refreshed on a timer rather than on demand. That timer is a deliberate trade: an
unknown
kiddoes not trigger a fetch, so an attacker cannot use junk tokens to drive traffic at your identity provider — at the cost of a window in which a newly-rotated key is rejected. Why a rotated signing key can be rejected for up to fifteen minutes works through it.
mTLS: the credential is the connection¶
Client-certificate auth does not implement Verifier, because a certificate is a
property of the TLS connection, not a string a caller passes in. It implements
CertVerifier
instead, taking the already-verified chains from the TLS stack
(tls.ConnectionState().VerifiedChains). authn derives an identity from a chain the
platform's TLS layer has already validated against your trust roots — it does not
re-implement certificate-path validation, and it rejects an empty chain.
What stays your responsibility¶
authn verifies credentials and derives identities. It does not:
- terminate TLS or manage certificates (see go/tls);
- extract the credential from the request, or shape the HTTP/gRPC error response — that transport glue is yours;
- store secrets, issue tokens, or run a policy engine.
Doing one thing — turning credentials into verified identities, safely — is what keeps it small enough to review and reuse everywhere.
The full list, including the credential types it cannot verify at all, is What authn does not do.
Where fail-closed does not apply: a stale JWKS¶
There is one place authn chooses availability over strictness. If the JWKS endpoint
cannot be reached and the cache still holds a key for the token's kid, that cached key
is used even though the cache is past its refresh interval. An identity-provider outage
therefore does not take your API down with it.
The consequence is that a key the issuer has withdrawn keeps verifying tokens here until
a fetch succeeds and replaces the cached set — authn has no way to learn about a
revocation out of band. If that is unacceptable in your threat model, shorten
RefreshInterval and alert on the operational error branch: a verify error that does
not wrap ErrUnauthenticated means the JWKS is unreachable, and it is the only signal
you get.