Skip to content

What authn does not do

authn is a small package with a deliberately narrow job: turn a credential into a verified identity, and let you decide what that identity may do. Almost everything else people associate with "auth" is out of scope.

This page lists what is absent, so you can find out here rather than halfway through an integration. Some of it is a design decision, some of it is simply unbuilt; where the distinction matters, it is stated.

Credential types it cannot verify

Credential Status
API key / shared secret Supported
JWT signed with an asymmetric key published in a JWKS Supported
Client certificate (mTLS) Supported
JWT signed with a shared secret (HS256 and friends) Not supported, and refused deliberately
Opaque tokens requiring introspection (RFC 7662) Not supported
HTTP Basic authentication Not supported
Session cookies Not supported
Signed-request schemes (AWS SigV4 and similar) Not supported

Symmetric-key JWTs are refused, not merely unimplemented

NewJWTVerifier requires a JWKS URL, and putting any HS* algorithm in AllowedAlgorithms fails construction outright:

authn: HMAC algorithm "HS256" is not allowed with a JWKS (alg-confusion defence)

A JWKS publishes public keys. If HMAC verification were also enabled, an attacker could take that public key, use it as the HMAC secret, sign a token of their choosing with alg: HS256, and have it verify. That is the classic algorithm-confusion attack, and the only reliable defence is to make the two modes mutually exclusive. There is no flag to override it.

If your issuer signs with a shared secret, authn cannot verify its tokens. Move the issuer to asymmetric signing, or verify those tokens yourself with golang-jwt directly.

Key material it cannot use

  • A public key you already have. There is no constructor that takes a crypto.PublicKey, a PEM file, or a static key set. Keys come from an HTTPS JWKS endpoint and nowhere else.
  • x5c / x5t JWKS entries. Only the raw key parameters are read — n and e for RSA, crv, x and y for EC. A JWKS that publishes certificates instead of raw key parameters yields no usable keys.
  • Ed25519 (OKP) keys, even though the underlying JWT library can verify EdDSA signatures. Only RSA and EC key types are parsed, and for EC only P-256, P-384 and P-521.
  • RSA-PSS by default. PS256/PS384/PS512 verify correctly but are not in the default AllowedAlgorithms, so you must list them explicitly.
  • A plaintext JWKS endpoint. HTTPS is required with no exemption, including for localhost and test servers. Testing against a local issuer means an httptest.NewTLSServer and passing its client via JWTConfig.HTTPClient.

OIDC: discovery only

WithOIDCDiscovery reads one thing out of /.well-known/openid-configuration — the jwks_uri — and nothing else. authn is a resource server, not a client.

There is no authorization-code flow, no PKCE, no token endpoint, no refresh-token handling, no userinfo call, no logout, and no nonce or at_hash validation. If you need a user to log in, that is a different library.

Token lifecycle it does not track

  • No revocation. A token is valid until its exp. There is no introspection call, no jti denylist and no way to invalidate a token early.
  • No maximum token age. The iat claim is not validated — not required, and not checked for being implausible. A token with iat two days in the future verifies fine.
  • sub is not required. A token without one authenticates successfully with an empty Identity.Subject.
  • One issuer per verifier. JWTConfig.Issuer is a single string and JWKSURL a single endpoint. Accepting tokens from two issuers means two verifiers and your own logic for choosing between them.

Authorization: a predicate, and two combinators

The whole authorization surface is AuthorizeFuncfunc(ctx, *Identity) bool — plus RequireScopes and RequireClaim.

There is no RBAC engine, no ABAC, no policy language, no rule file and no decision cache. That is a design decision rather than a gap: policy expressed as configuration becomes a second language to secure, review and audit, and every non-trivial deployment outgrows whatever model the library picked. The predicate is shaped as a hole you fill — evaluate a role map, call an external authorization service, whatever you need.

What that means in practice is that the combinators are thin:

  • There is no any-of, or or not combinator. RequireScopes is all-of only.
  • There is no "claim contains" test. RequireClaim is exact deep equality, so an array-valued claim like groups needs a hand-written predicate rather than a combinator.
  • RequireClaim always denies API-key and mTLS identities, because both have nil claims.

Transport work it leaves to you

authn has no HTTP or gRPC server imports, which is what makes it usable from both. The consequence is that everything touching a request is yours:

  • extracting the credential from an Authorization header, a metadata entry or a query parameter;
  • deciding which verifier applies to a given request;
  • turning a verify error into a 401 and a false predicate into a 403;
  • shaping the response body, and adding a WWW-Authenticate header if you want one;
  • terminating TLS, loading certificates and configuring a client CA pool — see go/tls.

The getting-started tutorial writes that glue in about twenty lines, and it is the same twenty lines every time.

There is also no helper for trying several verifiers in turn. Accepting both an API key and a JWT on the same endpoint is a switch you write.

API-key handling it does not provide

The API-key verifier compares keys in constant time and does nothing else. It does not:

  • generate, store, hash at rest, or rotate keys;
  • expire a key, or attach scopes or permissions to one;
  • rate-limit, lock out, or otherwise throttle repeated failures;
  • record an audit trail of use.

The key set is also fixed at construction. There is no method to add or remove an entry, so rotating a key means building a new verifier and swapping it in.

Two things it deliberately does not reject, which are easy to assume it would: duplicate keys across entries — the last matching entry wins — and an entry with an empty Subject.

Observability it does not emit

There is no logger, no metrics, no tracing and no callback hooks. Nothing reports a JWKS refresh succeeding or failing, a cache hit, or a rejected credential. Everything you want to see has to be recorded by the caller from what Verify returns.

This keeps the dependency footprint at two modules, which a guard test enforces — but it does mean a JWKS endpoint that has been failing for an hour is invisible unless you are logging the operational error branch. See Distinguishing a bad credential from a broken dependency.

Tuning it does not expose

Three values are package constants with no configuration path: the 30-second floor between JWKS fetch attempts, the 10-second fetch timeout, and the 1 MiB / 64-key caps on a JWKS document. Defaults and hard limits lists them with their failure modes.

Behaviour that surprises people, but is not a limitation

Two things get reported as bugs and are neither: