Guides

Authentication and authorization failures

Auth failures split into two questions the server asks in order: who are you? (authentication — fails as 401 or "invalid token") and are you allowed? (authorization — fails as 403 or "access denied"). Fixing the wrong one wastes the afternoon, so classify first.

401 family: the server doesn't believe you

Missing header, malformed header (Bearer prefix absent or doubled), an expired token, a token signed with the wrong key, or a token for the wrong audience/issuer. JWTs make this concrete — decode the payload (it is only base64, no secret needed) and read it:

echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{exp: (.exp | todate), aud, iss, scope}'

Check exp against the current time, and aud/iss against what the receiving service validates. Two non-obvious causes worth ruling out early: clock skew (a machine minutes off makes valid tokens "expired" or "not yet valid"), and rotated signing keys with a service still caching the old JWKS.

403 family: it believes you, and the answer is no

Credentials are valid but lack the specific permission: missing OAuth scope, IAM policy gap, role not granted, or resource-level rules (private repo, another tenant's object). The fix lives in the provider's permission model, not in your code — request the scope during authorization, grant the role, or use the identity that owns the resource. Note that some APIs return 404 instead of 403 for resources you can't see, to avoid leaking existence.

Failures that masquerade as bugs

API keys pasted with trailing newlines or wrapped in quotes; env vars set in your shell but not in the service's environment (CI, systemd, container); keys for the wrong environment (test vs live — Stripe-style prefixes make this checkable); OAuth refresh tokens silently revoked when a user changes their password. When a request works in curl but not in code, print the exact header your code sends — byte for byte — before suspecting the provider.

Handling it properly

On 401 with a refresh token: refresh once, retry once, and if that fails surface a re-authentication error — a refresh loop against a revoked grant is a rate-limit incident. On 403: never retry; log identity, resource, and required permission so the operator can fix the grant. And keep secrets out of logs — log the key's name or prefix, never the value.

Documented occurrences

93 analyzed errors across 27 libraries match this failure class. Each links to the thrown message, its source line, and documented fixes.

jackc/pgx

dotnet/aspnetcore

mongodb/node-mongodb-native

aio-libs/aiohttp

go-sql-driver/mysql

nestjs/nest

guzzle/guzzle

rust-lang/cargo

vitest-dev/vitest

gofiber/fiber

…and 17 more libraries — search for your exact message.

Other failure classes