API Authentication and Authorization Explained
By Oleksandr Andrushchenko — Published on — Modified on
API authentication verifies who or what is making a request. API authorization determines what that authenticated identity is allowed to do. They are related security layers, but they solve different problems and should be designed independently.
Production API security depends on more than accepting a bearer token. Secure systems validate credentials, verify token signatures and claims, enforce resource-level permissions, isolate tenants, rotate credentials, audit sensitive actions, and apply least privilege to both users and services.
Table of Contents
- Authentication vs Authorization
- Identity Model
- Session Authentication
- JWT Bearer Authentication
- API Key Authentication
- Mutual TLS Authentication
- Authentication Method Comparison
- OAuth 2.0 and OpenID Connect
- JWT Validation
- Role-Based Access Control
- Attribute-Based Access Control
- Relationship-Based Access Control
- Authorization Model Comparison
- Resource-Level Authorization
- Token Lifecycle
- Service-to-Service Authentication
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Conclusion
- More Articles to Read
Authentication vs Authorization
Authentication and authorization are often grouped into one security layer, but they answer different questions.
| Concern | Question | Example |
|---|---|---|
| Authentication | Who is making the request? | The access token represents user usr_123. |
| Authorization | What may that identity do? | User usr_123 may read order ord_456. |
Incoming request
-> Validate credentials
-> Build trusted identity context
-> Identify requested action
-> Load target resource
-> Evaluate authorization policy
-> Allow or deny
A request can be successfully authenticated and still be unauthorized. A valid customer token does not imply access to another customer’s records.
Key Point: authentication establishes identity; authorization evaluates permissions for an action and resource.
Identity Model
API security starts with a clear identity model. Callers may be users, backend services, workers, scripts, devices, CI/CD pipelines, or third-party integrations.
Different identities need different credential lifetimes, permissions, audit records, and revocation behavior.
Human Identities
Human identities represent people using browser applications, mobile applications, administrative tools, or command-line clients.
| Property | Example |
|---|---|
| Subject | usr_123 |
| Organization | org_456 |
| Roles | admin, billing_manager |
| Authentication strength | Password, MFA, passkey, enterprise SSO. |
Machine Identities
Machine identities represent services and automated workloads. They should not reuse human credentials.
orders-api
identity: svc_orders_api
permissions:
- payments.read
- payments.create
report-worker
identity: svc_report_worker
permissions:
- reports.read
- storage.write
Production Note: each service should have its own identity. Shared credentials make auditing, rotation, revocation, and least privilege difficult.
Authentication Context
Authentication should produce a structured identity context rather than a simple true/false result.
{
"subject": "usr_123",
"organization_id": "org_456",
"roles": ["billing_manager"],
"scopes": ["orders:read", "orders:write"],
"authentication_method": "mfa",
"session_id": "ses_8b371"
}
Roles, tenant IDs, and permissions must come from trusted credentials or server-side data. They should never be accepted directly from an ordinary request body.
Session Authentication
Session authentication stores authentication state on the server. The client receives an opaque session identifier, usually through a secure cookie.
Advantages
- Immediate revocation: deleting the session invalidates future requests.
- Small client credential: only an opaque identifier is sent.
- Central control: session state can be updated without reissuing credentials.
- Strong browser fit: secure, HTTP-only cookies reduce direct token exposure to JavaScript.
Disadvantages
- Shared storage: distributed applications need a shared session store.
- Lookup cost: requests usually require loading session state.
- CSRF risk: cookie-authenticated state-changing requests require CSRF protection.
- Cross-service complexity: session propagation across many APIs can become awkward.
When to Use / Real-World Use Cases
- First-party web applications using secure cookies.
- Administrative systems needing immediate logout or forced session termination.
- Applications with changing permissions that should take effect immediately.
- Systems where central session auditing is more important than stateless validation.
Example
User signs in
-> Server validates credentials
-> Server creates session in Redis
-> Browser receives HTTP-only secure cookie
-> Request includes cookie
-> API loads session and identity context
JWT Bearer Authentication
JWT bearer authentication uses signed tokens containing claims. APIs can verify those tokens using a trusted signing key.
Advantages
- Distributed validation: APIs can validate tokens without a central session lookup.
- Standardized claims: issuer, audience, subject, scopes, and expiration have common meanings.
- Strong API interoperability: widely supported by API gateways, identity providers, and frameworks.
- Good mobile and service fit: bearer tokens work naturally outside browser-cookie flows.
Disadvantages
- Harder revocation: valid tokens normally remain usable until expiration.
- Larger credentials: every request carries signed claims.
- Validation complexity: issuer, audience, algorithm, expiration, and scopes must all be verified.
- Stale claims: role or permission changes may not apply until a new token is issued.
When to Use / Real-World Use Cases
- Mobile applications calling backend APIs.
- Public REST APIs using OAuth 2.0 access tokens.
- Microservices validating tokens issued by a shared authorization server.
- API gateways performing token validation before requests reach services.
Example
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
API validation:
- verify signature
- validate issuer
- validate audience
- validate expiration
- validate scopes
- build identity context
API Key Authentication
API keys are long-lived credentials used to identify applications, customers, integrations, or projects.
Advantages
- Simple integration: easy for external systems and scripts.
- Easy usage attribution: requests can be associated with a customer or application.
- Useful for rate limiting: limits can be applied per key.
- Low protocol complexity: no full OAuth flow is required.
Disadvantages
- Often long-lived: leaked keys may remain valid for months.
- Limited identity context: keys often represent an application, not a specific user.
- Manual rotation: external integrations may not update credentials quickly.
- Unsafe in public clients: browser or mobile binaries cannot reliably protect static keys.
When to Use / Real-World Use Cases
- Server-to-server public API integrations.
- Developer APIs where customers manage integration credentials.
- Usage tracking and quota enforcement.
- Low-complexity internal tools with narrowly scoped access.
Example
X-API-Key: sk_live_7f91a2...
Production Note: store key hashes rather than plaintext values when possible, show secrets only once, use recognizable prefixes, and support overlap during rotation.
Mutual TLS Authentication
Mutual TLS authenticates both client and server using certificates. The client verifies the server certificate, and the server verifies the client certificate.
Advantages
- Strong machine identity: authentication happens during TLS connection establishment.
- Encrypted communication: identity and transport security are combined.
- Reduced bearer-token theft risk: possession of a token alone is not enough.
- Good regulatory fit: commonly used in high-trust environments.
Disadvantages
- Certificate lifecycle complexity: issuance, renewal, revocation, and trust chains must be managed.
- Operational overhead: debugging failed certificate negotiation can be difficult.
- Client compatibility: browser and third-party integration support may vary.
- Infrastructure dependency: proxies and gateways must preserve verified client identity safely.
When to Use / Real-World Use Cases
- Banking and financial integrations.
- Internal service meshes.
- Partner APIs requiring certificate-bound access.
- Regulated systems needing strong workload identity.
Example
Client
-> presents client certificate
Server
-> verifies trusted certificate authority
TLS connection
-> established only after both sides authenticate
Authentication Method Comparison
Authentication methods should be selected from client type, revocation requirements, credential lifetime, and operational maturity.
| Method | Best Fit | Main Advantage | Main Trade-Off |
|---|---|---|---|
| Session | First-party web applications | Immediate revocation | Shared state and CSRF protection |
| JWT | Mobile, public APIs, distributed services | Distributed validation | Revocation and claim staleness |
| API Key | Third-party server integrations | Simplicity | Long-lived credential risk |
| Mutual TLS | High-trust machine communication | Strong certificate identity | Certificate operations |
OAuth 2.0 and OpenID Connect
OAuth 2.0 is an authorization framework. OpenID Connect extends OAuth with authentication and standardized identity information.
What OAuth 2.0 Solves
OAuth allows a client to access an API on behalf of a user or itself without receiving the user’s password.
User
-> Authorization Server
-> Grants consent
-> Client receives access token
-> Client calls API
-> API validates audience and scope
Authorization Code Flow
The authorization code flow is the standard interactive OAuth flow. Public clients should use PKCE.
1. Client generates PKCE verifier and challenge
2. User is redirected to authorization server
3. User authenticates and grants consent
4. Authorization server returns short-lived code
5. Client exchanges code and verifier for tokens
6. Client calls API with access token
OpenID Connect
OpenID Connect adds identity through an ID token and standardized claims.
| Token | Consumer | Purpose |
|---|---|---|
| Access token | API | Authorize protected operations. |
| ID token | Client application | Describe the authenticated login. |
| Refresh token | Client application | Obtain new access tokens. |
Important: APIs should receive access tokens. ID tokens should not be used as general API credentials.
JWT Validation
Decoding a JWT only reads its payload. Validation proves that the token was issued by a trusted authority and is valid for the current API.
JWT Structure
header.payload.signature
| Section | Purpose |
|---|---|
| Header | Algorithm and key identifier. |
| Payload | Claims such as issuer, subject, audience, and expiration. |
| Signature | Protects the header and payload from modification. |
JWT claims are encoded, not encrypted. Sensitive secrets should not be placed in ordinary signed JWTs.
Signature Validation
Signature validation should use a trusted public key and an explicitly allowed algorithm.
JWT header contains key ID
-> API loads matching key from trusted JWKS
-> API verifies signature using allowed algorithm
-> Invalid tokens are rejected
Important: never accept unsigned tokens or trust the token-provided algorithm without server-side restrictions.
Claim Validation
| Claim | Required Validation |
|---|---|
| iss | Must match the trusted issuer. |
| aud | Must include the current API. |
| exp | Must be in the future. |
| nbf | Token must already be active. |
| sub | Must contain a valid caller identifier. |
| scope | Must include the operation’s required permission. |
JWKS Caching and Key Rotation
APIs using asymmetric JWT signing usually fetch public keys from a JWKS endpoint.
- Cache keys to avoid a network request on every API call.
- Respect cache headers when the identity provider supplies them.
- Refresh on unknown key ID to support signing-key rotation.
- Fail safely when the key endpoint is unavailable and no valid cached key exists.
Production Note: key rotation should be tested before the first emergency rotation is required.
Role-Based Access Control
RBAC assigns permissions to roles, then assigns roles to users or services.
Advantages
- Easy to understand: roles map naturally to organizational responsibilities.
- Simple administration: permissions are managed centrally.
- Good auditability: role assignments are straightforward to review.
- Strong enterprise fit: roles such as admin, support, and billing manager are common.
Disadvantages
- Role explosion: many combinations produce too many roles.
- Weak contextual rules: roles alone cannot express ownership or location constraints well.
- Coarse permissions: users may receive more access than needed.
- Difficult exceptions: one-off business rules often bypass the role model.
When to Use / Real-World Use Cases
- Administrative dashboards.
- SaaS applications with organization-level roles.
- Internal enterprise systems.
- Applications with stable permission groups.
Example
Role: billing_manager
Permissions:
invoices.read
invoices.create
payments.refund
Attribute-Based Access Control
ABAC evaluates user, resource, environment, and request attributes.
Advantages
- Fine-grained: policies can include department, clearance, region, or risk level.
- Dynamic: access can change from current attributes rather than static role assignments.
- Reduces role explosion: policies replace many specialized roles.
- Strong policy expressiveness: complex business rules can be modeled directly.
Disadvantages
- Harder to reason about: many attributes interact.
- More difficult auditing: effective permissions may not be obvious.
- Policy testing required: subtle combinations can create unexpected access.
- Data dependency: inaccurate attributes lead to incorrect authorization.
When to Use / Real-World Use Cases
- Healthcare systems with patient, provider, and location rules.
- Financial platforms with risk-based access.
- Enterprise document systems using department and classification.
- Multi-region systems with geographic restrictions.
Example
Allow access if:
user.department == document.department
AND
user.clearance_level >= document.classification_level
AND
request.region IN user.allowed_regions
Relationship-Based Access Control
Relationship-Based Access Control evaluates how identities relate to resources and other identities.
Advantages
- Natural sharing model: permissions follow ownership and collaboration.
- Good hierarchy support: access can flow through organizations, projects, and teams.
- Strong resource context: policies reflect real relationships.
- Good large-product fit: collaborative systems often need relationship graphs.
Disadvantages
- Graph complexity: authorization checks may require relationship traversal.
- Caching difficulty: relationship changes can invalidate many decisions.
- Operational complexity: policy engines and relationship stores may be required.
- Harder debugging: access can be inherited through several levels.
When to Use / Real-World Use Cases
- Shared documents and folders.
- Source-code repositories.
- Project-management tools.
- Social and collaboration platforms.
Example
User
-> member of Team
-> Team owns Project
-> Project contains Document
Result:
User may read Document
Authorization Model Comparison
Authorization models can be combined. RBAC may establish broad responsibilities, while ABAC or relationship checks enforce resource-level rules.
| Model | Best Fit | Main Advantage | Main Trade-Off |
|---|---|---|---|
| RBAC | Stable organizational roles | Simplicity | Role explosion |
| ABAC | Contextual enterprise policies | Fine-grained rules | Policy complexity |
| Relationship-based | Collaborative resource sharing | Natural ownership model | Relationship graph complexity |
Resource-Level Authorization
Authentication and broad scopes are not enough. APIs should verify access to every protected resource.
Ownership Checks
# Load the order using both resource ID and authenticated owner.
# This prevents accidentally returning another user's order.
order = repository.get_order(
order_id=order_id,
customer_id=current_user.id,
)
if order is None:
# Return 404 to avoid revealing whether another user's order exists.
raise HTTPException(status_code=404, detail="Order not found")
Tenant Isolation
Multi-tenant queries should always include the trusted tenant identifier.
-- tenant_id comes from the validated identity context,
-- never from an untrusted request body.
SELECT *
FROM orders
WHERE tenant_id = :tenant_id
AND order_id = :order_id;
Important: missing tenant filters are a common cause of cross-customer data exposure.
Preventing IDOR
IDOR occurs when an API accepts a resource ID and returns the resource without checking whether the caller may access it.
GET /orders/ord_123
Required checks:
- token is valid
- caller has orders:read
- order belongs to caller's tenant
- caller owns order or has elevated role
Token Lifecycle
Token security depends on issuance, storage, expiration, refresh, revocation, and rotation.
Access Token Expiration
Access tokens should usually be short-lived. Five to thirty minutes is common, depending on risk and client behavior.
Short lifetimes reduce exposure if a token is stolen.
Refresh Tokens
Refresh tokens obtain new access tokens without another interactive login.
Login
-> Access token: 15 minutes
-> Refresh token: longer lifetime
Access token expires
-> Client presents refresh token
-> Authorization server rotates refresh token
-> Client receives new token pair
Revocation and Rotation
- Rotate refresh tokens after successful use.
- Revoke token families when reuse is detected.
- Rotate signing keys without breaking active validation.
- Disable compromised API keys immediately.
- Audit credential issuance and revocation.
Secure Token Storage
| Client Type | Preferred Storage |
|---|---|
| Browser web app | Secure HTTP-only cookie or carefully designed in-memory token model. |
| Mobile app | Platform secure storage such as Keychain or Keystore. |
| Backend service | Workload identity or secrets manager. |
| CLI tool | OS-protected credential storage. |
Production Note: local storage exposes tokens to JavaScript and increases the impact of cross-site scripting vulnerabilities.
Service-to-Service Authentication
Backend services need independent identities and narrowly scoped permissions.
Client Credentials
OAuth client credentials are useful when one backend service calls another through an authorization server.
orders-api
-> Authorization Server
-> Access token with payments:create
-> payments-api
Workload Identities
Cloud workload identities eliminate static credentials.
| Platform | Mechanism |
|---|---|
| AWS | IAM roles for ECS tasks, Lambda, and EC2. |
| Kubernetes | Service accounts and workload identity integration. |
| Google Cloud | Workload Identity. |
| Azure | Managed Identities. |
Signed Requests
Signed-request protocols authenticate the caller and protect selected request components from tampering.
HTTP method
+ request path
+ selected headers
+ payload hash
+ timestamp
-> cryptographic signature
AWS Signature Version 4 is a common example.
Ready-to-Use Example
The following examples show a practical JWT-protected Orders API with scopes, ownership checks, consistent errors, JWKS caching, and an API Gateway JWT authorizer.
OpenAPI Security Example
openapi: 3.0.3
info:
title: Orders API
version: "1.0"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: OAuth 2.0 access token issued for the Orders API.
schemas:
ErrorResponse:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
- request_id
properties:
code:
type: string
message:
type: string
request_id:
type: string
paths:
/v1/orders/{order_id}:
get:
summary: Get an order
security:
- bearerAuth: []
parameters:
- name: order_id
in: path
required: true
schema:
type: string
responses:
"200":
description: Order returned.
"401":
description: Missing or invalid access token.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"403":
description: Token is valid but required scope is missing.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Order does not exist or is not visible to the caller.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
FastAPI JWT Example
from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Any
import httpx
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from jose import JWTError, jwt
app = FastAPI(title="Orders API")
ISSUER = "https://auth.example.com"
AUDIENCE = "orders-api"
JWKS_URL = f"{ISSUER}/.well-known/jwks.json"
ALLOWED_ALGORITHMS = ["RS256"]
# Cache public signing keys so every API request does not call the identity provider.
_jwks_cache: dict[str, Any] | None = None
_jwks_cache_expires_at = 0.0
@dataclass(frozen=True)
class Identity:
subject: str
tenant_id: str
scopes: frozenset[str]
roles: frozenset[str]
ORDERS = {
"ord_123": {
"id": "ord_123",
"tenant_id": "org_456",
"customer_id": "usr_123",
"status": "paid",
}
}
class ApiError(Exception):
def __init__(self, status_code: int, code: str, message: str):
self.status_code = status_code
self.code = code
self.message = message
@app.middleware("http")
async def add_request_id(request: Request, call_next):
# Preserve caller request IDs when available so distributed traces stay connected.
request.state.request_id = request.headers.get("x-request-id", "req_generated")
response = await call_next(request)
response.headers["x-request-id"] = request.state.request_id
return response
@app.exception_handler(ApiError)
async def handle_api_error(request: Request, exc: ApiError):
# Return one stable error shape across authentication and authorization failures.
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"request_id": request.state.request_id,
}
},
)
async def get_jwks(force_refresh: bool = False) -> dict[str, Any]:
global _jwks_cache
global _jwks_cache_expires_at
now = time.time()
# Refresh when cache expires or an unknown key ID appears during rotation.
if force_refresh or _jwks_cache is None or now >= _jwks_cache_expires_at:
async with httpx.AsyncClient(timeout=3.0) as client:
response = await client.get(JWKS_URL)
response.raise_for_status()
_jwks_cache = response.json()
# A short cache keeps validation available while still supporting key rotation.
_jwks_cache_expires_at = now + 300
return _jwks_cache
async def find_signing_key(token: str) -> dict[str, Any]:
try:
header = jwt.get_unverified_header(token)
except JWTError as exc:
raise ApiError(401, "invalid_token", "Access token is malformed.") from exc
key_id = header.get("kid")
algorithm = header.get("alg")
# Never allow the token to choose an unexpected algorithm.
if algorithm not in ALLOWED_ALGORITHMS:
raise ApiError(401, "invalid_token", "Token algorithm is not allowed.")
for force_refresh in (False, True):
jwks = await get_jwks(force_refresh=force_refresh)
for key in jwks.get("keys", []):
if key.get("kid") == key_id:
return key
raise ApiError(401, "invalid_token", "Signing key was not found.")
async def decode_access_token(token: str) -> dict[str, Any]:
signing_key = await find_signing_key(token)
try:
# Verify signature plus issuer, audience, and expiration.
return jwt.decode(
token,
signing_key,
algorithms=ALLOWED_ALGORITHMS,
issuer=ISSUER,
audience=AUDIENCE,
options={
"verify_signature": True,
"verify_exp": True,
"verify_aud": True,
"verify_iss": True,
},
)
except JWTError as exc:
raise ApiError(401, "invalid_token", "Access token is invalid or expired.") from exc
async def get_identity(
authorization: str | None = Header(default=None),
) -> Identity:
if not authorization or not authorization.startswith("Bearer "):
raise ApiError(401, "unauthenticated", "Bearer token is required.")
token = authorization.removeprefix("Bearer ").strip()
claims = await decode_access_token(token)
subject = claims.get("sub")
tenant_id = claims.get("tenant_id")
if not subject or not tenant_id:
raise ApiError(401, "invalid_token", "Required identity claims are missing.")
scopes = frozenset(str(claims.get("scope", "")).split())
roles = frozenset(claims.get("roles", []))
return Identity(
subject=subject,
tenant_id=tenant_id,
scopes=scopes,
roles=roles,
)
def require_scope(required_scope: str):
async def dependency(identity: Identity = Depends(get_identity)) -> Identity:
if required_scope not in identity.scopes:
raise ApiError(
403,
"insufficient_scope",
f"Required scope is missing: {required_scope}.",
)
return identity
return dependency
@app.get("/v1/orders/{order_id}")
async def get_order(
order_id: str,
identity: Identity = Depends(require_scope("orders:read")),
):
order = ORDERS.get(order_id)
# Apply tenant isolation before returning any resource data.
if not order or order["tenant_id"] != identity.tenant_id:
raise ApiError(404, "order_not_found", "Order was not found.")
# Resource ownership is still required after tenant and scope validation.
is_owner = order["customer_id"] == identity.subject
is_admin = "admin" in identity.roles
if not is_owner and not is_admin:
# 404 avoids revealing that another user's resource exists.
raise ApiError(404, "order_not_found", "Order was not found.")
return {
"id": order["id"],
"status": order["status"],
}
Python Client Example
import requests
from uuid import uuid4
API_BASE_URL = "https://api.example.com"
ACCESS_TOKEN = "replace-with-short-lived-access-token"
request_id = f"req_{uuid4().hex[:8]}"
response = requests.get(
f"{API_BASE_URL}/v1/orders/ord_123",
headers={
# Access tokens belong in the Authorization header.
"Authorization": f"Bearer {ACCESS_TOKEN}",
# Request IDs connect client errors with server logs.
"X-Request-ID": request_id,
},
# Every production HTTP call needs an explicit timeout.
timeout=3,
)
if response.status_code == 401:
print("Token is invalid or expired.")
elif response.status_code == 403:
print("Token is valid but does not have enough permission.")
elif response.status_code == 404:
print("Order does not exist or is not visible to this caller.")
else:
response.raise_for_status()
print(response.json())
CloudFormation JWT Authorizer
AWSTemplateFormatVersion: "2010-09-09"
Description: HTTP API with JWT authorization
Parameters:
JwtIssuer:
Type: String
Description: Trusted OpenID Connect issuer URL.
JwtAudience:
Type: String
Description: Audience claim expected by this API.
Resources:
OrdersApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: orders-api
ProtocolType: HTTP
OrdersJwtAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref OrdersApi
# JWT authorizers validate signed access tokens before Lambda or ECS receives traffic.
AuthorizerType: JWT
# Read the bearer token from the standard Authorization header.
IdentitySource:
- "$request.header.Authorization"
Name: orders-jwt-authorizer
JwtConfiguration:
# API Gateway validates that the access token was issued for this API.
Audience:
- !Ref JwtAudience
# API Gateway retrieves signing keys from the issuer's discovery metadata.
Issuer: !Ref JwtIssuer
GetOrderRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref OrdersApi
RouteKey: "GET /v1/orders/{order_id}"
# Associate the route with the JWT authorizer.
AuthorizationType: JWT
AuthorizerId: !Ref OrdersJwtAuthorizer
# Require an explicit scope before traffic reaches the backend.
AuthorizationScopes:
- orders:read
# Replace with a real HTTP or Lambda integration target.
Target: "integrations/replace-with-integration-id"
Production Note: gateway-level validation reduces invalid traffic reaching the application, but resource ownership and tenant authorization still belong in the backend.
Common Mistakes
API security mistakes usually come from validating only part of the request or treating authentication as complete authorization.
- Decoding JWTs without verifying signatures.
- Skipping issuer or audience validation.
- Using ID tokens as API access tokens.
- Issuing long-lived access tokens.
- Storing tokens insecurely in browser local storage.
- Sharing API keys between services or customers.
- Checking scopes without checking resource ownership.
- Accepting tenant IDs from request payloads.
- Embedding authorization logic inconsistently across controllers.
- Failing to test signing-key rotation.
- Logging access tokens, refresh tokens, or API keys.
- Returning detailed security errors that reveal protected resources.
Production Checklist
A production API security model should validate credentials, enforce least privilege, protect token lifecycle, and authorize each resource access.
- Separate authentication and authorization.
- Use HTTPS everywhere.
- Validate JWT signature, issuer, audience, expiration, and algorithm.
- Cache JWKS keys and support rotation.
- Issue short-lived access tokens.
- Rotate refresh tokens after use.
- Store browser credentials securely.
- Hash API keys where possible.
- Give every service its own identity.
- Prefer workload identities over static cloud credentials.
- Validate required scopes or permissions.
- Apply resource ownership checks.
- Filter every multi-tenant query by trusted tenant context.
- Audit privileged and security-sensitive actions.
- Never log secrets or bearer tokens.
- Test credential revocation and signing-key rotation.
Conclusion
API authentication establishes trusted identity, while API authorization determines what that identity may do. Secure systems keep these concerns separate and enforce both consistently.
Session authentication, JWTs, API keys, and mutual TLS each solve different client and infrastructure problems. RBAC, ABAC, and relationship-based authorization provide different ways to model permissions. The correct design depends on client type, risk, scale, revocation needs, and resource relationships.
Key Takeaway: validate every credential, restrict every identity, and authorize every protected resource. A valid token proves identity; it does not prove access.
More Articles to Read
- REST API Design for Production Systems
- API Rate Limiting and Throttling Explained
- API Pagination, Filtering, and Sorting Explained
- API Versioning and Backward Compatibility
- API Performance and Reliability Best Practices
Comments (0)