Auth and Scopes
Series
API Design MasteryAuthentication and authorization are the most consequential design decisions in your API. Get them wrong and you spend the rest of your API's life adding workarounds: custom middleware, ad-hoc permission checks scattered through business logic, shadow tables tracking who can do what. Get them right up front and the rest of your security posture falls into place naturally. This post covers the three layers that matter: bearer tokens for identity, OAuth scopes for capability, and mTLS for machine-to-machine trust.
Bearer Tokens: The Baseline
Every API call must carry a credential. For HTTP APIs, the de-facto standard is Bearer Token via the Authorization header:
GET /orders/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...Never put tokens in query parameters — they end up in server logs, browser history, and referrer headers. The header is the only safe channel.
JWTs are the most common token format. A well-structured JWT payload for an API:
{
"sub": "usr_01J8K2MNPQ3RS4TUV5WX6YZ7A",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"exp": 1729123456,
"iat": 1729119856,
"scope": "orders:read orders:write inventory:read",
"org": "org_acme",
"jti": "jwt_01J8K2MNPQ"
}Validate every field: iss, aud, exp, and jti. The jti claim enables token revocation via a deny-list without invalidating the entire token family.
Designing OAuth Scopes
Scopes answer the question: "What is this token allowed to do?" Bad scope design either grants too much (admin) or creates maintenance nightmares through too many fine-grained scopes.
A practical naming convention: resource:action
orders:read
orders:write
orders:delete
inventory:read
inventory:write
users:read
users:write
billing:read
billing:writeGroup scopes into tiers for the authorization code flow:
Implication (where write implies read for the same resource) reduces the number of scopes a client must request to do useful work. Document implication explicitly; do not rely on clients to infer it.
For machine-to-machine flows (Client Credentials grant), scopes represent service-level capabilities rather than per-user permissions:
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=svc_inventory_worker
&client_secret=...
&scope=inventory:read+orders:writeEnforcing Scopes in Middleware
Scope checking belongs in a single, auditable layer — not scattered through handler code:
# Pseudocode — framework-agnostic
def require_scope(*required_scopes):
def decorator(handler):
def wrapper(request, *args, **kwargs):
token_scopes = set(request.auth.scopes)
if not token_scopes.issuperset(required_scopes):
raise InsufficientScopeError(
required=required_scopes,
granted=token_scopes
)
return handler(request, *args, **kwargs)
return wrapper
return decorator
@require_scope("orders:write")
def create_order(request):
...Return a 403 (not 401) when a valid token lacks the required scope:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
WWW-Authenticate: Bearer error="insufficient_scope",
scope="orders:write"
{
"type": "https://api.example.com/problems/insufficient-scope",
"title": "Insufficient Scope",
"status": 403,
"detail": "Token does not have the required 'orders:write' scope.",
"requiredScopes": ["orders:write"],
"grantedScopes": ["orders:read"]
}mTLS for Machine-to-Machine APIs
At the service mesh or API gateway level, mutual TLS provides a second authentication factor: the client's certificate proves identity independent of any token. This is the right choice for:
- Internal service-to-service communication
- Partner APIs with high-value SLAs
- Payment, healthcare, or regulated-data endpoints
mTLS handshake flow:
The gateway terminates mTLS and forwards the verified client identity as a header. Your application never handles raw certificates — it trusts the gateway's header:
GET /internal/inventory/42 HTTP/1.1
X-Client-Cert-Subject: CN=svc-order-processor,O=Example,C=US
X-Client-Cert-Fingerprint: sha256:aa:bb:cc:...
Authorization: Bearer eyJ...Require both a valid cert and a valid token for the highest-sensitivity endpoints (defense in depth).
Token Expiry and Rotation
Short-lived access tokens (15 minutes) limit blast radius when a token is stolen. Pair with refresh tokens:
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=rt_01J8K2MN...Rotate refresh tokens on each use (refresh token rotation). Detect replay by checking if a refresh token is used after it has already been rotated — if so, revoke the entire token family.
Key Takeaways
- Always send bearer tokens in the
Authorizationheader — never in query parameters or cookie values for API access. - Validate all JWT claims (
iss,aud,exp,jti) on every request; do not trust the payload without signature verification. - Design scopes as
resource:actionpairs and group them into tiers; let write imply read for the same resource. - Enforce scope checking in a single middleware layer, not in individual handlers — scattered checks are a maintenance and audit hazard.
- Return 403 with a machine-readable
insufficient_scopeerror when a valid token lacks required scopes; return 401 only when authentication itself failed. - Use mTLS at the gateway for machine-to-machine APIs, especially in regulated or high-value contexts, and pair it with short-lived tokens for defense in depth.
Series
API Design Mastery