Verifiers reference¶
The three constructors, what they accept, when they refuse to build, and exactly what
each one puts on the returned Identity.
NewAPIKeyVerifier¶
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
Keyand differentSubjectvalues 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 producesIdentity.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¶
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:
- Apply any
JWTOption. - Fill in defaults for
Leeway,RefreshIntervalandAllowedAlgorithms. - Build the HTTP client (
cfg.HTTPClient, or a new one with a 10-second timeout). - If
WithOIDCDiscoverywas given, fetch the discovery document and setJWKSURLandIssuerfrom it. - Require a non-empty
JWKSURL. - Require a non-empty
Issuer. - Validate
AllowedAlgorithms. - Parse
JWKSURLand require thehttpsscheme. - 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
keysmember 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¶
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.
issuerURLmust use thehttpsscheme.- The document is fetched from
issuerURLwith any trailing/trimmed, plus/.well-known/openid-configuration. - The document's
issuervalue must equal the string you passed, byte for byte. - On success,
cfg.JWKSURLis set to the document'sjwks_uriandcfg.Issueris set to the document'sissuer.
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¶
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¶
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.