Choose & configure a verifier¶
authn ships three verifiers. Pick by the credential your clients present:
| Credential | Constructor | Interface |
|---|---|---|
| Shared API key / token | NewAPIKeyVerifier |
Verifier |
| JWT (OIDC / OAuth2 bearer) | NewJWTVerifier |
Verifier |
| Client certificate (mTLS) | NewMTLSVerifier |
CertVerifier |
API-key and JWT verifiers satisfy Verifier
(Verify(ctx, credential string)). mTLS uses
CertVerifier
because a certificate is a transport property, not a credential string.
API keys¶
NewAPIKeyVerifier
takes a set of {Key, Subject} entries and matches presented keys in constant time:
verifier, err := authn.NewAPIKeyVerifier(
authn.KeyEntry{Key: "s3cr3t", Subject: "ci-bot"},
authn.KeyEntry{Key: "adm1n", Subject: "admin"},
)
The resulting Identity.Method is "apikey" and Subject is the matched entry's
label. Claims and Scopes are always nil, so a claim- or scope-based authorization
check will never admit an API-key identity.
Construction fails in exactly two cases: no entries at all, and an entry whose Key is
empty. It does not reject duplicate keys — two entries sharing a Key build fine
and the last one wins — and it does not reject an empty Subject, which produces an
identity with an empty subject. Full behaviour and error strings are in the
verifiers reference.
JWT / OIDC¶
NewJWTVerifier
validates a signed JWT: the signature against a cached JWKS, then exp, nbf, iss
and aud. The verified Claims and parsed Scopes land on the Identity.
Two of those are mandatory on the token itself: a JWT with no exp claim, or no iss
claim, is rejected. nbf is checked only when present. iat is not checked at all, and
neither is sub — a token without a subject verifies successfully with an empty
Identity.Subject. exp and nbf are both compared with Leeway (default 60s) of
tolerance.
Point it at a JWKS endpoint directly:
verifier, err := authn.NewJWTVerifier(ctx, authn.JWTConfig{
Issuer: "https://issuer.example.com",
Audiences: []string{"my-api"},
JWKSURL: "https://issuer.example.com/.well-known/jwks.json",
})
…or let OIDC discovery resolve the JWKS URL from the issuer's
/.well-known/openid-configuration:
verifier, err := authn.NewJWTVerifier(ctx,
authn.JWTConfig{Audiences: []string{"my-api"}},
authn.WithOIDCDiscovery("https://issuer.example.com"),
)
Issuer is omitted from that second example on purpose. Discovery sets both JWKSURL
and Issuer from the document, overwriting anything you configured — so setting
Issuer alongside WithOIDCDiscovery looks like belt and braces and is actually
ignored.
Getting the OIDC issuer URL exactly right¶
The URL you pass to WithOIDCDiscovery is compared byte for byte against the issuer
value in the discovery document. A trailing slash is enough to fail construction:
authn: OIDC document issuer "https://issuer.example.com" does not match "https://issuer.example.com/"
Fetch https://<issuer>/.well-known/openid-configuration yourself and copy the
issuer field out of it verbatim.
JWKS caching and algorithm pinning¶
RefreshInterval (default 15m) is how long a fetched key set is treated as current, not
a floor on fetch frequency. While the cache is current, a token whose kid is not in it
is rejected without a refresh being attempted — so a signing key added mid-interval
is not picked up until the interval elapses. Why a rotated signing key can be rejected
for up to fifteen minutes covers what to do about
that.
AllowedAlgorithms defaults to the asymmetric RS/ES 256/384/512 set. none and any
HS* algorithm are refused at construction rather than at verification, so there is no
configuration in which the algorithm-confusion attack is reachable.
PS256/PS384/PS512 verify correctly but must be listed explicitly. Spell algorithm names in upper case — the accept list is matched against the
token header verbatim, so "rs256" builds fine and then rejects every token.
The JWKS endpoint must be HTTPS, with no exemption for localhost. Testing against a
local issuer means an httptest.NewTLSServer and passing its client through
JWTConfig.HTTPClient.
mTLS¶
NewMTLSVerifier
derives an identity from the client certificate chain the TLS stack already verified.
Because the credential is the connection, it implements CertVerifier —
VerifyCert(ctx, verifiedChains) — which you call from your transport with
tls.ConnectionState().VerifiedChains:
By default Identity.Subject is the leaf certificate's Common Name; if that is empty,
the first DNS SAN; if that is empty too, the first URI SAN. Note that this is the CN
alone, not the full RDN sequence of the certificate subject. Override how the subject
is derived — for example to use a SAN URI or a specific RDN — with
WithCertSubject:
cv := authn.NewMTLSVerifier(
authn.WithCertSubject(func(c *x509.Certificate) string {
if len(c.URIs) > 0 {
return c.URIs[0].String() // SPIFFE-style identity
}
return c.Subject.CommonName
}),
)
VerifyCert rejects an empty chain, so a connection without a presented client
certificate fails closed. It also rejects a certificate whose subject derivation returns
an empty string — including when your own WithCertSubject function returns "", so
make sure yours has a fallback.
VerifyCert does no cryptographic work of its own: chain building, trust-root
validation and expiry are the TLS stack's job, and the server must be configured with
tls.RequireAndVerifyClientCert and a ClientCAs pool for VerifiedChains to be
populated at all. Handing it a chain you assembled yourself skips the check entirely.
Accepting more than one credential type¶
The verifiers are independent values, and there is no built-in helper for trying several in turn. To accept more than one credential type, run the applicable verifier for the credential the transport extracted and treat success as authenticated:
switch {
case len(connState.VerifiedChains) > 0:
id, err = mtlsVerifier.VerifyCert(ctx, connState.VerifiedChains)
case strings.Count(bearer, ".") == 2:
id, err = jwtVerifier.Verify(ctx, bearer)
default:
id, err = apiKeyVerifier.Verify(ctx, bearer)
}
Each returns a uniform Identity, so the rest of your stack — and your
authorization check — does not care which one succeeded.
Identity.Method tells it if it does care.
Prefer choosing one verifier per request over running them all and taking the first success. Trying every verifier leaves you with several errors and no clean answer to "why was this rejected", and it puts every credential through every code path for no gain.