Skip to main content
API Design Mastery

Auth and Scopes

Ravinder··4 min read
API DesignRESTGraphQLgRPCSecurity
Share:
Auth and Scopes

Authentication 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:write

Group scopes into tiers for the authorization code flow:

graph TD A[Scopes] --> B[Read-Only Tier] A --> C[Write Tier] A --> D[Admin Tier] B --> B1["orders:read"] B --> B2["inventory:read"] B --> B3["users:read"] C --> C1["orders:write"] C --> C2["inventory:write"] C --> C3["orders:read (implied)"] D --> D1["users:write"] D --> D2["billing:write"] D --> D3["All write scopes (implied)"]

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:write

Enforcing 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:

sequenceDiagram participant Client as Service A participant Gateway participant API as Service B Client->>Gateway: ClientHello Gateway-->>Client: ServerCertificate Client-->>Gateway: ClientCertificate Gateway->>Gateway: Verify client cert against trusted CA Gateway->>API: Forward with X-Client-Cert-Subject header API-->>Gateway: Response Gateway-->>Client: Response

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 Authorization header — 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:action pairs 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_scope error 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.
Share: