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

MechanismGood forWeakness
API key in a headerServer-to-server with a known partnerNo expiry unless you build one; leaks in logs
HTTP Basic over TLSInternal tools, quick integrationCredentials sent on every request
Client credentials (OAuth 2.0)Machine-to-machine at scaleToken caching and rotation complexity
Authorization code + PKCEUser-facing apps and SPAsRequires a redirect flow and a callback URL
Signed JWTStateless verification across servicesRevocation is hard; claims can go stale
mTLSHigh-value internal APIsCertificate 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 auth
GET /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 iss and aud. 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.

Securing REST APIs beyond authentication Versioning and error shapes

Last refreshed 2026-09-18.