Authentication and authorisation
Choosing between API keys, OAuth 2.0 and signed tokens, and validating what you accept rather than trusting what it claims.
Choosing a mechanism
| Mechanism | Good for | Weakness |
|---|---|---|
| API key in a header | Server-to-server with a known partner | No expiry unless you build one; leaks in logs |
| HTTP Basic over TLS | Internal tools, quick integration | Credentials sent on every request |
| Client credentials (OAuth 2.0) | Machine-to-machine at scale | Token caching and rotation complexity |
| Authorization code + PKCE | User-facing apps and SPAs | Requires a redirect flow and a callback URL |
| Signed JWT | Stateless verification across services | Revocation is hard; claims can go stale |
| mTLS | High-value internal APIs | Certificate lifecycle management |
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=orders:write&client_id=svc-billing
# client_secret goes in the body or, preferably, in HTTP Basic authGET /orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtfMSJ9...Validating a token properly
import jwt
from jwt import PyJWKClient
JWKS = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
ISSUER = "https://auth.example.com/"
AUDIENCE = "https://api.example.com"
def verify(token: str) -> dict:
key = JWKS.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
key,
algorithms=["RS256"], # never trust the alg header alone
issuer=ISSUER,
audience=AUDIENCE,
options={"require": ["exp", "iat", "iss", "aud", "sub"]},
)
def require_scope(claims: dict, scope: str) -> None:
granted = set((claims.get("scope") or "").split())
if scope not in granted:
raise PermissionError("missing scope " + scope)- Verify the signature with the keys you fetched from the issuer's JWKS endpoint, and cache those keys rather than fetching per request.
- alg=none is the classic attack: a decoder that honours the header's algorithm can be tricked into accepting an unsigned token. Always pass an explicit algorithm allowlist.
- Check
issandaud. A valid token for another service is not a valid token for yours. - Never use a token's claims as your only authorisation. Ownership is a data question: load the resource and check the subject against it.
- Keep access tokens short-lived and use refresh tokens or client credentials to get new ones.
- An API key in a query string ends up in access logs, browser history and referrer headers. Use a header.
⚠️
Authentication answers who is calling; authorisation answers what this call may do to this object. An API that checks the token and then trusts a client-supplied
userId in the body has authentication but no authorisation — that is the most common broken-access-control finding in real APIs.FAQ
Where should a browser app keep a token?
In memory with a refresh token in an HttpOnly, Secure, SameSite cookie. Local storage is readable by any script on the page, so a single third-party script compromise leaks it.
How do I revoke a JWT before it expires?
You cannot, by design. Keep lifetimes short, maintain a small deny-list of rejected token identifiers for the highest-risk cases, and rely on refresh-token revocation for the rest.
Related
Securing REST APIs beyond authentication Versioning and error shapes
Last refreshed 2026-09-18.