Errors reference¶
Every error string authn produces, what causes it, and what to change. Strings are
quoted as the package emits them; %q-style placeholders show where a value you
supplied is interpolated.
Errors fall into two groups that you should treat completely differently:
- Construction errors are configuration problems. They surface once, at
NewAPIKeyVerifier/NewJWTVerifiertime, and should stop your process starting. - Verification errors all wrap
ErrUnauthenticatedand mean the same thing on the wire: a generic401. The detail is for your log.
Construction errors — API key¶
| Error | Cause | Fix |
|---|---|---|
authn: API-key verifier requires at least one key (fail-closed) |
NewAPIKeyVerifier() called with no entries |
Supply at least one KeyEntry. An empty set is refused because it would mean "accept anything". |
authn: API-key entry 1 has an empty key |
a KeyEntry with Key: ""; the number is the zero-based index |
Populate the key, or drop the entry. A missing environment variable is the usual cause. |
Construction errors — JWT¶
| Error | Cause | Fix |
|---|---|---|
authn: JWT verifier requires a JWKS URL (set JWKSURL or WithOIDCDiscovery) |
neither JWKSURL nor WithOIDCDiscovery supplied |
Set one of them. |
authn: JWT verifier requires an Issuer |
Issuer empty and discovery not used |
Set JWTConfig.Issuer to the exact iss your tokens carry. |
authn: algorithm "none" is never allowed |
AllowedAlgorithms contains none in any letter case |
Remove it. There is no override. |
authn: HMAC algorithm "HS256" is not allowed with a JWKS (alg-confusion defence) |
AllowedAlgorithms contains a value starting HS in any letter case; the message echoes the value you supplied |
Remove it. A JWKS publishes public keys, and accepting HMAC would let an attacker use the public key as the shared secret. |
authn: parse JWKS URL: parse "://bad": missing protocol scheme |
JWKSURL is not a URL |
Correct the URL. |
authn: JWKS URL must be HTTPS, got "http" |
JWKSURL uses a scheme other than https |
Use HTTPS. There is no exemption for localhost or a test server. |
authn: initial JWKS fetch: … |
the priming fetch failed; the wrapped cause follows | See the JWKS fetch errors below. |
Construction errors — OIDC discovery¶
All of these come from WithOIDCDiscovery.
| Error | Cause | Fix |
|---|---|---|
authn: parse OIDC issuer URL: … |
the issuer URL is not a URL | Correct it. |
authn: OIDC issuer URL must be HTTPS, got "http" |
the issuer URL uses another scheme | Use HTTPS. |
authn: build OIDC discovery request: … |
the derived discovery URL could not be turned into a request | Check the issuer URL for stray characters. |
authn: fetch OIDC discovery document: … |
the request failed — DNS, TLS, connection, or the 10-second deadline | Check reachability and TLS trust from the running process, not from your laptop. |
authn: OIDC discovery returned 404 |
the issuer served a non-200 for /.well-known/openid-configuration |
Confirm the issuer URL is the OIDC issuer, not the login page or the API base. |
authn: read OIDC discovery document: … |
the body could not be read | Usually a connection dropped mid-response. |
authn: parse OIDC discovery document: … |
the body is not valid JSON, or exceeded 1 MiB and was truncated | Fetch the URL yourself and look at what comes back — an HTML error page is the common cause. |
authn: OIDC document issuer "https://issuer.example.com" does not match "https://issuer.example.com/" |
the document's issuer differs from the string you passed, byte for byte |
Pass the issuer exactly as the document advertises it. A trailing slash is the usual culprit — see A trailing slash on the issuer URL breaks discovery. |
JWKS fetch errors¶
These surface at construction (wrapped in authn: initial JWKS fetch:) and again on
any later refresh. On a refresh they are only returned to a caller when there is no
usable cached key for the requested kid — otherwise the cached key is used and the
failure is invisible.
| Error | Cause |
|---|---|
authn: build JWKS request: … |
the JWKS URL could not be turned into a request |
authn: fetch JWKS: … |
DNS, TLS, connection failure, or the 10-second deadline |
authn: JWKS endpoint returned 404 |
any non-200 status |
authn: read JWKS body: … |
the body could not be read |
authn: JWKS document exceeds 1048576 bytes |
the document is larger than 1 MiB |
authn: parse JWKS document: … |
the body is not valid JSON |
authn: JWKS document has 65 keys, max 64 |
the document publishes more than 64 keys |
Errors from an individual key — an unsupported kty, an unknown EC curve, an
out-of-range RSA exponent, bad base64 — are never returned. That key is skipped and
the rest of the document is kept, which is what allows a JWKS containing a key type
authn does not support to keep working. The cost is that a document with no usable
keys at all looks like a successful fetch.
Verification errors¶
Every one of these wraps ErrUnauthenticated, so errors.Is(err, authn.ErrUnauthenticated)
is true for all of them, and every one maps to the same generic 401 on the wire.
| Error | Cause |
|---|---|
authn: unauthenticated |
an API key that matched no configured entry. There is no further detail by design. |
token is malformed: token contains an invalid number of segments: authn: unauthenticated |
the credential is not a JWT — an empty string or arbitrary text produces this |
token has invalid claims: token is missing required claim: exp claim is required: authn: unauthenticated |
the token has no exp claim; exp is mandatory |
token has invalid claims: token is expired: authn: unauthenticated |
exp is in the past by more than Leeway |
token has invalid claims: token is not valid yet: authn: unauthenticated |
nbf is in the future by more than Leeway |
token has invalid claims: token is missing required claim: iss claim is required: authn: unauthenticated |
the token has no iss claim |
token has invalid claims: token has invalid issuer: authn: unauthenticated |
iss does not equal JWTConfig.Issuer |
token is unverifiable: error while executing keyfunc: no JWKS key for kid "…": authn: unauthenticated: authn: unauthenticated (the quoted value is the token's kid, or "" if it has no kid header) |
no key in the cached JWKS matches the token's kid. The sentinel appears twice because the key lookup and the verify call each wrap it. |
token signature is invalid: crypto/rsa: verification error: authn: unauthenticated |
the signature does not verify against the key found for that kid |
token signature is invalid: signing method RS256 is invalid: authn: unauthenticated |
the token's alg is not in AllowedAlgorithms. Also produced by a case mismatch — listing "rs256" rejects RS256 tokens. |
audience not accepted: authn: unauthenticated |
Audiences is set and none of the token's aud values matches; also produced when the token has no aud claim, or when aud is neither a string nor an array of strings |
unexpected claims type: authn: unauthenticated |
the parsed claims were not jwt.MapClaims; not reachable through the public API |
no verified client certificate: authn: unauthenticated |
VerifyCert received an empty chain set, or a first chain with no certificates — meaning the TLS stack verified no client certificate |
client certificate has no usable subject: authn: unauthenticated |
the leaf has no Common Name, no DNS SAN and no URI SAN — or your WithCertSubject function returned "" |
Distinguishing a bad credential from a broken dependency¶
Both come back from Verify as errors, but only rejections wrap the sentinel:
switch {
case err == nil:
// authenticated
case errors.Is(err, authn.ErrUnauthenticated):
// a rejected credential — expected traffic, log at info or debug
default:
// operational: the JWKS could not be fetched and no cached key was usable
// — log at error and alert on it
}
The second branch is the one that fills logs during an attack; the third is the one that
means your issuer is unreachable. Both still return 401 to the caller.