Skip to content

Verifiers reference

The three constructors, what they accept, when they refuse to build, and exactly what each one puts on the returned Identity.

NewAPIKeyVerifier

func NewAPIKeyVerifier(entries ...KeyEntry) (Verifier, error)

Returns a Verifier that accepts any of the supplied keys. Each KeyEntry is a {Key, Subject} pair: Key is the shared secret the caller presents, Subject is the label recorded on the resulting Identity.

On success Verify returns Identity{Subject: <the matched entry's Subject>, Method: "apikey"}. Claims and Scopes are always nil — an API key carries no claims, so RequireClaim always denies an API-key identity and RequireScopes admits it only when called with no scopes.

On failure it returns ErrUnauthenticated with no additional detail — the error string is exactly authn: unauthenticated.

Construction fails in two cases, and only two:

Condition Error
No entries at all authn: API-key verifier requires at least one key (fail-closed)
Any entry with an empty Key authn: API-key entry 1 has an empty key (the number is the zero-based index)

What NewAPIKeyVerifier does not reject

  • Duplicate keys. Two entries with the same Key and different Subject values build successfully. At verification time the last matching entry wins, so {Key: "dup", Subject: "first"}, {Key: "dup", Subject: "second"} authenticates as "second". Nothing warns you.
  • An empty Subject. The entry is accepted and produces Identity.Subject == "". A predicate comparing subjects will then match the empty string, so set a label on every entry.
  • Weak or short keys. There is no minimum length, no entropy check and no expiry — key quality and rotation are yours.

Timing behaviour

Comparison is constant-time in a stronger sense than subtle.ConstantTimeCompare alone: the verifier compares a fixed-width SHA-256 digest of the presented credential against the pre-computed digest of every configured key, and iterates all entries with no early return. Neither match/no-match, key length, nor which key matched is distinguishable by timing.

The ctx argument to Verify is accepted for interface conformance and never read — API-key verification does no I/O and cannot be cancelled.

NewJWTVerifier

func NewJWTVerifier(ctx context.Context, cfg JWTConfig, opts ...JWTOption) (Verifier, error)

Returns a Verifier for JWT bearer tokens, with signing keys fetched from a JWKS endpoint. Field-by-field configuration is in JWTConfig fields.

Construction happens in this order, and the first failure stops it:

  1. Apply any JWTOption.
  2. Fill in defaults for Leeway, RefreshInterval and AllowedAlgorithms.
  3. Build the HTTP client (cfg.HTTPClient, or a new one with a 10-second timeout).
  4. If WithOIDCDiscovery was given, fetch the discovery document and set JWKSURL and Issuer from it.
  5. Require a non-empty JWKSURL.
  6. Require a non-empty Issuer.
  7. Validate AllowedAlgorithms.
  8. Parse JWKSURL and require the https scheme.
  9. Fetch the JWKS once, so a bad endpoint fails here rather than on the first request.

On success Verify returns Identity{Subject: <the "sub" claim>, Method: "jwt", Claims: <all verified claims>, Scopes: <parsed from "scope" or "scp">}.

On failure it returns ErrUnauthenticated wrapped with the underlying reason, for your log only. See Verification errors for the full set.

What the initial fetch does and does not catch

Step 9 catches an unreachable endpoint, a non-200 response, an oversized body, and a document that is not valid JSON. It does not catch a document that parses but yields no usable keys. All three of these construct successfully and then reject every token at request time with no JWKS key for kid "…":

  • {"keys":[]}
  • a document with no keys member at all, such as {"hello":"world"}
  • a document whose keys are all of an unsupported type, such as {"keys":[{"kty":"oct","kid":"a"}]}

authn supports "RSA" and "EC" key types only, and for EC only the P-256, P-384 and P-521 curves. An unsupported or unparseable key is skipped silently and the rest of the document is kept.

Duplicate kid values in a JWKS

Keys are stored in a map keyed by kid, so if a document publishes two keys under the same kid, the later one wins and tokens signed by the earlier one fail with token signature is invalid. Providers that publish both the old and new key during a rotation normally give them distinct kid values; one that does not will break verification for the older key.

Tokens with no kid header

A token with no kid header looks up the empty string. If the JWKS also publishes a key with no kid, that key is stored under "" and the lookup succeeds — a single-key JWKS with no kid therefore works. Otherwise verification fails with no JWKS key for kid "".

WithOIDCDiscovery

func WithOIDCDiscovery(issuerURL string) JWTOption

Resolves JWKSURL from the issuer's /.well-known/openid-configuration document at construction time. This is the only OIDC affordance in the package: there is no login flow, no authorization-code exchange and no token-endpoint use.

  • issuerURL must use the https scheme.
  • The document is fetched from issuerURL with any trailing / trimmed, plus /.well-known/openid-configuration.
  • The document's issuer value must equal the string you passed, byte for byte.
  • On success, cfg.JWKSURL is set to the document's jwks_uri and cfg.Issuer is set to the document's issuer.

A trailing slash on the issuer URL breaks discovery

The trailing / is trimmed when building the discovery URL but not when comparing the issuer, so passing https://issuer.example.com/ against a provider that advertises "issuer": "https://issuer.example.com" fails construction with:

authn: OIDC document issuer "https://issuer.example.com" does not match "https://issuer.example.com/"

Pass the issuer exactly as the provider advertises it in its own discovery document.

Discovery overwrites a configured Issuer without telling you

WithOIDCDiscovery assigns the document's issuer to cfg.Issuer, replacing anything you set. Setting JWTConfig{Issuer: "https://pinned.example"} and passing WithOIDCDiscovery("https://other.example") constructs without error, and the verifier then accepts tokens issued by https://other.example. Set Issuer or use discovery, not both — and if you need the issuer pinned to a value you control, set it directly and supply JWKSURL yourself.

The same applies to JWKSURL: a value you set is replaced by the document's jwks_uri.

NewMTLSVerifier

func NewMTLSVerifier(opts ...MTLSOption) CertVerifier

Returns a CertVerifier that derives an Identity from an already-verified client certificate chain. It has no error return — construction cannot fail.

VerifyCert(ctx, verifiedChains) takes the chains from a completed TLS handshake, as in tls.ConnectionState.VerifiedChains, and reads the leaf of the first chain (chains[0][0]).

On success it returns Identity{Subject: <derived>, Method: "mtls"}. Claims and Scopes are always nil.

On failure:

Condition Error
chains is empty, or its first chain is empty no verified client certificate: authn: unauthenticated
The subject function returns "" client certificate has no usable subject: authn: unauthenticated

The ctx argument is accepted for interface symmetry and never read.

What VerifyCert does not do

It performs no cryptographic validation of its own. Chain building, trust-root validation, expiry and revocation are the TLS stack's job — configure the server with tls.RequireAndVerifyClientCert and a ClientCAs pool. VerifiedChains is populated only when the TLS stack required and verified a client certificate, which is what makes an empty set safe to treat as "no credential".

Passing a chain you built yourself rather than one the TLS stack verified defeats the whole check, because authn will trust it.

WithCertSubject

func WithCertSubject(fn func(*x509.Certificate) string) MTLSOption

Overrides how Identity.Subject is derived from the leaf certificate.

The default tries, in order: the Common Name; the first DNS SAN; the first URI SAN. If all three are absent it returns "", and VerifyCert then fails with client certificate has no usable subject. Note that this is the CN specifically, not the full RDN sequence of the certificate subject.

Passing nil is a no-op. WithCertSubject(nil) leaves the default in place rather than panicking or clearing the function, so a conditionally-built option cannot accidentally disable subject derivation.

A function returning "" for a certificate you meant to admit turns into a 401, not a passthrough — fail closed applies to your override too.