Skip to content

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 Verifier turns a credential into a verified Identity.
  • Authorization (what may you do?) — an AuthorizeFunc predicate turns an Identity (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 Verifier must 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 generic 401 / 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. AllowedAlgorithms defaults to the asymmetric RS/ES 256/384/512 set. Both none and every HS* 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. exp and iss are required: a token carrying neither is rejected outright, so a token that never expires cannot be minted by omitting the claim. nbf is checked when present. aud must be one of the configured audiences — leaving Audiences empty disables that check entirely, which means accepting tokens minted for a different service by the same issuer. exp and nbf both allow a configurable Leeway (default 60s) for clock skew. iat is 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 kid does 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.