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. The verifier will not accept an unsigned token, and pinning algorithms prevents an attacker swapping an RS256 public key into an HS256 MAC.
  • Full standard-claim validation. iss must match; aud must be one of the configured audiences (leaving it empty disables the check and is discouraged); exp/nbf are enforced with a small configurable Leeway (default 60s) for clock skew.
  • Keys from a trusted JWKS over HTTPS. Signing keys are fetched from the configured (or OIDC-discovered) JWKS endpoint, which must be HTTPS, and cached with a bounded RefreshInterval so key rotation is picked up without refetching per request.

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.