API Security & OAuth 2.0
Security is a property of the whole request path, not a middleware checkbox. Two questions must be answered separately:
- Authentication (AuthN): who is calling?
- Authorisation (AuthZ): are they allowed to do this, on this object, now?
Most API breaches are authorisation failures, not broken cryptography.
OAuth 2.0 in one page
OAuth delegates access without sharing passwords. Roles:
- Resource owner — the user.
- Client — the app requesting access.
- Authorization server — issues tokens after consent/auth.
- Resource server — your API, validates tokens.
Authorization Code + PKCE (browser/mobile)
Best for user-facing apps:
- Client generates
code_verifier+code_challenge. - Redirects to the authorization server with
client_id,redirect_uri,scope,state,code_challenge. - User authenticates/consents.
- Server redirects back with
code(andstate). - Client exchanges
code+code_verifierfor tokens at the token endpoint. - Client calls the API with
Authorization: Bearer <access_token>.
PKCE prevents authorization-code interception. state prevents CSRF on the callback.
Client Credentials (service-to-service)
No user context. The service authenticates with its client id/secret (or mTLS/private key JWT) and receives a token scoped to its own permissions.
Device Code (TVs/CLI)
The device shows a code, the user approves on another device, the device polls for tokens.
Flows to avoid
- Implicit — tokens in URLs; deprecated.
- Resource Owner Password — apps handling user passwords; deprecated.
- Long-lived tokens without refresh.
Tokens
| JWT (self-contained) | Opaque (reference) | |
|---|---|---|
| Validation | local signature/claims | introspection/lookup |
| Revocation | hard before expiry | immediate |
| Size | larger | small |
| Best for | short-lived access tokens | refresh tokens, high-security revocation |
JWT rules:
- validate signature,
iss,aud,exp,nbf, andscope; - never trust
alg: none; pin accepted algorithms; - keep access tokens short-lived (5–15 min);
- do not put secrets or excessive PII in claims (they are readable);
- support key rotation via
kidand JWKS.
Refresh tokens:
- store server-side (hashed), rotate on use, detect reuse;
- bind to client/device where possible;
- revoke on logout/password change.
Scopes, roles, permissions
- Scopes limit what a token can do at the API level (
orders:read,orders:write). - Roles group permissions for humans (
support,admin). - Object-level permissions decide per resource (
order.customerId === user.id).
A token with orders:read still must not read another tenant's order. Enforce tenancy at the query level, not just in the handler.
async function getOrder(ctx: Context, id: OrderId): Promise<Order> {
const order = await orders.findById(id);
if (!order) throw new NotFound();
if (order.tenantId !== ctx.tenantId) throw new NotFound(); // do not leak existence
if (!ctx.user.can("order:read", order)) throw new Forbidden();
return order;
}
OWASP API Security risks (the ones that matter)
| Risk | Example | Defence |
|---|---|---|
| BOLA (object-level authz) | /orders/123 returns another user's order |
check ownership/tenant on every object |
| Broken authentication | weak tokens, no expiry, credential stuffing | short-lived tokens, MFA, rate limits |
| Broken object property authz | mass assignment sets role: admin |
allowlist mutable fields |
| Excessive data exposure | returning full user rows | response DTOs, field-level filtering |
| Broken function authz | admin endpoint only hidden in UI | server-side role checks |
| Unrestricted resource consumption | expensive queries, large uploads | limits, quotas, timeouts |
| SSRF | fetch arbitrary URL from server | allowlist, block private IPs |
| Security misconfiguration | verbose errors, open CORS, debug endpoints | safe defaults, headers, reviews |
| Injection | SQL/NoSQL/command | parameterised queries, validation |
| Improper inventory | forgotten v1 endpoint | API inventory/deprecation |
Cookies, CSRF, CORS
- Cookies:
Secure,HttpOnly,SameSite=Lax/Strict, scopedPath, short expiry. - State-changing cookie-authenticated requests need CSRF protection (double-submit token or SameSite + origin checks).
- CORS is not authorisation. Allow specific origins, methods, headers; never
*with credentials. - Bearer tokens in browser storage are XSS-sensitive; prefer memory + refresh cookie, or BFF pattern.
API keys and mTLS
- API keys identify a client, not a user. They are not sufficient for authorisation by themselves; combine with scopes/IP allowlists and rotate them.
- mTLS authenticates the client certificate; common in B2B and high-security service meshes. Requires certificate lifecycle management.
- Never put keys in URLs, git, or client-side code.
Rate limiting and abuse
Security controls fail without limiting:
- per-IP and per-account limits;
- login/OTP attempt limits (credential stuffing);
- payload/upload size caps;
- expensive query budgets;
- anomaly detection for scraping.
Logging and privacy
- Log auth decisions (allow/deny) with caller id and reason, not tokens.
- Redact secrets, PII, and authorization headers.
- Keep an audit trail for sensitive actions (who, what, when, from where).
- GDPR/CCPA: data minimisation, retention, deletion.
Checklist
- AuthN and AuthZ are separate decisions, enforced server-side.
- Every object access checks tenant/ownership (BOLA).
- Scopes are validated, not just token presence.
- Access tokens are short-lived; refresh tokens rotate and can be revoked.
- JWTs validate signature, alg, iss, aud, exp.
- Mass assignment is blocked by DTO allowlists.
- Responses expose only necessary fields.
- Rate limits and payload caps exist on auth, search, and uploads.
- CORS, cookies, and CSRF are configured for the browser model.
- Secrets are in a secret manager, rotated, and never logged.
Next: Versioning & Pagination — evolving a contract without breaking callers.