API Authentication and Authorization¶
A backend API is not secure merely because a request contains a valid token. The service must establish who or what is calling, decide whether that principal may perform the requested action on the specific resource, validate the input, and preserve a compatible contract as the API evolves.
This guide focuses on the boundaries between those decisions. It uses OAuth 2.0 and OpenID Connect for identity and delegated access, HTTP semantics for API behavior, and a multi-tenant order service for practical authorization examples.
1. Keep four decisions separate¶
A request passes through several distinct checks:
- Parsing: Is the request well-formed, within size limits, and encoded using a supported media type?
- Validation: Do its fields have the expected shape and do their values make sense for this operation?
- Authentication: Which user or workload is making the request, and how was that identity verified?
- Authorization: May that principal perform this action on this resource in the current state?
Only after these checks should the application execute the business operation.
These are not interchangeable controls. A syntactically valid account ID does not prove that the caller owns the account. A valid access token does not grant every permission. A request that passes a JSON schema can still violate a business invariant.
A useful internal model is an immutable principal created by trusted authentication middleware:
- subject identifier;
- tenant or organization membership resolved by the application;
- granted scopes or permissions;
- authentication method and relevant assurance information;
- token or session metadata needed for policy decisions.
Do not construct this principal from fields in the request body. The caller may choose a resource identifier, but the application must derive identity and authority from verified credentials and trusted server-side data.
2. Choose the right authentication mechanism¶
User sign-in: OpenID Connect¶
OAuth 2.0 is an authorization framework. By itself, it does not define a standard user-authentication protocol. OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0 and defines the ID Token used by a client to verify an authenticated end user.
For a browser or mobile application, use an authorization-code flow with PKCE and a trusted identity provider. Follow the current OAuth 2.0 Security Best Current Practice:
- use exact registered redirect URI matching;
- use PKCE with the S256 challenge method;
- protect the authorization response against CSRF and mix-up attacks;
- do not use the implicit grant or the resource-owner-password-credentials grant for new systems;
- keep client secrets out of public clients such as browser applications and native apps.
PKCE is not a substitute for correct redirect URI validation, issuer selection, or token validation. Use a maintained OIDC client library rather than implementing the protocol by hand.
Do not confuse an ID Token with an access token¶
These tokens answer different questions:
| Token | Intended consumer | Purpose |
|---|---|---|
| OIDC ID Token | The relying-party client | Carries authentication claims about the end user |
| OAuth access token | The resource server / API | Grants access according to the authorization server's contract |
| Refresh token | The client and authorization server | Obtains replacement access tokens under the granted conditions |
An ID Token is not an API access token. Do not accept it at an API merely because it is a signed JWT. The API should validate an access token issued for that API, including its intended audience and token profile.
An access token may be opaque or may use a JWT format. OAuth does not require JWTs. The resource server must follow the authorization server's documented validation or introspection contract.
Service-to-service authentication¶
When one workload calls another without a user acting in the request, use a workload identity or an OAuth client-credentials flow supported by the platform. Give the workload a distinct identity, restrict its audience and scopes, and prefer short-lived credentials.
Avoid copying one broad, long-lived API key into every service. A compromised worker should not automatically gain the same authority as the entire application fleet. Where available, use platform workload identity federation or sender-constrained credentials such as mutual TLS or DPoP for higher-risk integrations.
API keys have a narrower job¶
An API key can identify a project, integration, or calling application. It does not automatically identify the human using that application, and it should not become a substitute for user-level authorization.
If API keys are necessary:
- issue separate keys per integration and environment;
- scope them to the smallest required operations;
- show the secret only at creation and store a verifier or hash rather than plaintext;
- support expiry, rotation, and revocation;
- rate-limit by key and by relevant account or tenant;
- never place keys in query strings, URLs, source code, or logs.
If the application stores passwords¶
Prefer a managed identity provider when it fits the product. If the application verifies passwords itself, use a maintained password-hashing implementation, not a general-purpose fast hash such as SHA-256. OWASP currently recommends Argon2id for new password storage and gives a minimum configuration of 19 MiB memory, two iterations, and parallelism one; benchmark and tune it for the deployment.
NIST SP 800-63B-4 requires a minimum of 15 characters for passwords used as a single authentication factor, permits a lower minimum of eight when the password is used only as part of multi-factor authentication, and advises against arbitrary composition rules and routine forced password changes. Apply a compromised/common-password blocklist and rate-limit failed attempts. Authentication throttling must not create an easy account-lockout denial of service.
3. Protect sessions and tokens¶
Browser sessions¶
A server-side session can be represented by a high-entropy, opaque cookie value that maps to session state held by the application or a shared session store.
For a cookie-based session:
- set Secure and HttpOnly;
- choose SameSite deliberately for the product's navigation and federation flows;
- rotate the session identifier after login and privilege changes;
- enforce idle and absolute expiry;
- revoke the server-side session on logout, account disablement, or security events;
- use CSRF tokens and/or strict Origin checks for state-changing requests where cookies are sent automatically.
SameSite is useful defense in depth, but it should not be treated as a universal replacement for a CSRF design. Cookie-based authentication and bearer-token authentication have different browser threat models.
Bearer access tokens¶
A bearer token grants access to whoever possesses it. Send it in the Authorization header over TLS:
Do not put bearer tokens in query parameters. URLs are commonly copied into browser history, analytics, reverse-proxy logs, and referrer data. Redact Authorization and Cookie headers from application and infrastructure logs.
JWT validation is more than decoding¶
A signed JWT is integrity-protected, not encrypted by default. Its payload is normally readable by anyone who obtains it. Do not put secrets or sensitive personal data in ordinary signed JWT claims.
Use a maintained JOSE library and validate the token against a profile configured by the application:
- Accept only an explicit algorithm allowlist configured for the trusted issuer. Do not select an algorithm solely because the untrusted token header asks for it.
- Verify the signature with a key obtained through the configured issuer's trusted key-discovery process.
- Validate the exact issuer (iss) and the intended audience (aud).
- Validate expiration (exp), not-before (nbf) when present, and any required issued-at (iat) policy.
- Validate the token type and required claims for this API. Do not accept an ID Token where an access token is expected.
- Reject malformed, unsupported, expired, wrongly issued, or wrongly targeted tokens.
Do not follow arbitrary key URLs supplied in a token header. A token-controlled jku or x5u URL can turn key discovery into an SSRF path. A kid value is also untrusted input; use it only to select among keys from the configured issuer's trusted key set.
A valid signature only proves that a trusted key signed the token. It does not prove that the token is intended for this API, that the account is still active, or that the requested action is authorized.
Opaque tokens versus JWTs¶
| Choice | Strengths | Costs and failure modes |
|---|---|---|
| Opaque token with introspection | Centralized policy and revocation; token contents are not exposed to clients | Adds a network dependency, latency, and availability requirements |
| Short-lived signed JWT | Local verification can reduce per-request dependency calls | Revocation and permission changes are not immediate unless additional controls exist; key rotation and claim validation are operational responsibilities |
Choose based on revocation needs, latency, availability, and the trust boundaries between services. Stateless validation is not automatically simpler or safer. If permissions can change during a token's lifetime, use short lifetimes, introspection, session revocation, or another explicit freshness mechanism.
4. Authorize the action on the actual resource¶
Authentication establishes a principal. Authorization evaluates a policy over at least:
- the principal;
- the requested action;
- the resource;
- the resource's tenant or ownership context;
- relevant environmental or business attributes.
A role check at the route level is not enough for many APIs. A user who can read orders may still be allowed to read only their own orders. A tenant administrator may read other orders in that tenant but not another tenant's data.
RBAC, ABAC, and relationship-based checks¶
- Role-based access control (RBAC): permissions are grouped into roles such as support-agent or billing-operator. It is understandable and works well for broad job functions.
- Attribute-based access control (ABAC): a decision depends on attributes such as tenant, region, data classification, time, or resource state. It supports contextual policy but can become difficult to reason about if rules are scattered.
- Relationship-based access control (ReBAC): access follows relationships such as owner, team member, project collaborator, or organization administrator. It fits collaborative products but requires careful relationship lifecycle and consistency design.
These models can be combined. For example, a principal may need the billing-operator role, belong to the order's tenant, and satisfy a resource-state rule before a refund is permitted.
OAuth scopes are useful for coarse API capabilities. They do not replace object-level checks. A scope such as orders:read does not imply permission to read every order identifier the caller can guess.
Prevent broken object-level authorization¶
Consider:
The server must not assume that possession of ord_123 or a valid orders:read scope grants access. Resolve the object within the authenticated tenant and then apply the resource policy.
A tenant-scoped database query can reduce accidental cross-tenant access:
SELECT id, tenant_id, owner_id, status
FROM orders
WHERE id = :order_id
AND tenant_id = :authenticated_tenant_id;
Both values must be bound parameters. The tenant value must come from the verified principal and trusted membership resolution, not from an untrusted request field. Depending on the product's disclosure policy, a missing or cross-tenant resource can return 404 to avoid revealing that another tenant's object exists.
The database filter is defense in depth, not a reason to omit application authorization. For stronger isolation, PostgreSQL row-level security can add another enforcement layer, but connection-pool session state and transaction boundaries must be handled carefully. Test that a reused connection cannot retain the previous tenant's context.
Protect writable properties too¶
Do not deserialize arbitrary client JSON directly into a privileged persistence model. A caller might add fields such as tenant_id, owner_id, is_admin, approved, or balance even when the UI never exposes them.
Use request DTOs or explicit field allowlists. Derive ownership and tenant fields on the server. Authorize sensitive property changes separately from ordinary updates, and validate state transitions in the domain layer.
A safe update is not simply "the user may update this order." It may be "the user may change the shipping address while the order is pending, but cannot change its tenant, owner, payment status, or refund state."
Deny by default and check every request¶
Centralize enforcement where practical, but keep the decision close enough to the protected resource that it has the object and tenant context. A policy decision point can evaluate rules; a policy enforcement point must still enforce the result.
- deny when a rule is missing or evaluation fails;
- authorize every request, including background and internal API calls;
- re-check permission after role, membership, or ownership changes;
- avoid caching authorization decisions longer than the underlying attributes remain valid;
- make cross-tenant access an explicit, separately reviewed capability.
OWASP identifies broken object-level authorization as a major API risk because endpoints frequently use caller-supplied object identifiers. Treat every identifier-based read, update, delete, export, and action endpoint as an authorization boundary.
5. Validate requests before business processing¶
Validation should happen on the server even if the frontend performs the same checks. Apply resource limits before expensive parsing: maximum body size, nesting depth, array length, string length, upload size, and request duration.
Validate both:
- Syntax: type, format, required fields, allowed enum values, and structure.
- Semantics: relationships and domain rules, such as quantity greater than zero, end time after start time, or a transition allowed from the current order state.
Prefer allowlists and explicit schemas over attempts to remove suspicious characters. Do not use validation as a substitute for parameterized SQL, output encoding, safe deserialization, or authorization. A valid identifier remains untrusted until access is checked.
For partial updates, define the difference between an omitted field, an explicit null, and an empty value. Reject unexpected fields when accepting them could silently change privileged state. Version request schemas deliberately so old clients are not accidentally broken by stricter validation.
Use HTTP status codes consistently¶
- 400 Bad Request: malformed request syntax or invalid framing.
- 422 Unprocessable Content: the content is understood but its instructions cannot be processed, for example a syntactically valid payload that violates a documented field constraint.
- 401 Unauthorized: credentials are missing or invalid. An origin server must include an applicable WWW-Authenticate challenge.
- 403 Forbidden: credentials are valid, but the caller is not allowed to perform the operation.
- 404 Not Found: the resource does not exist, or the API deliberately conceals its existence from this caller.
- 409 Conflict: the operation conflicts with the current resource state.
Use RFC 9457 Problem Details for stable machine-readable error responses when it fits the API. Do not expose stack traces, token contents, SQL, internal hostnames, or sensitive resource details in an error body.
6. Version APIs without creating avoidable breakage¶
HTTP does not prescribe one universal application API versioning convention. Choose a convention, document it, and apply it consistently.
| Strategy | Example | Trade-off |
|---|---|---|
| URI major version | /v1/orders | Easy to discover, route, document, and operate side by side; can encourage unnecessary version duplication |
| Media-type versioning | Accept: application/vnd.example.v2+json | Keeps the resource URI stable; less visible and can complicate caches, clients, and tooling |
| Explicit version header | API-Version: 2026-01 | Separates version from the URI; requires clear documentation and correct cache variation |
A practical default for public or independently deployed APIs is a URI major version for breaking contract changes. Do not create a new major version for every additive field or internal implementation change.
Compatibility rules matter more than the syntax:
- add optional response fields without changing existing field meanings;
- do not remove or rename fields while supported clients still depend on them;
- do not change units, default values, ordering, or error semantics silently;
- treat new enum values carefully because clients may use exhaustive switches;
- keep request validation compatible with documented older clients;
- publish migration notes and test representative old client payloads;
- measure version usage before retirement.
Deprecation and retirement are separate steps. RFC 9745 defines the Deprecation response header to signal that a resource is or will be deprecated; the header itself does not change resource behavior. RFC 8594 defines the Sunset header to indicate that a resource is expected to become unavailable at a future time. Publish a migration guide, provide a realistic overlap period, monitor remaining clients, and communicate the actual removal date.
7. A small, testable authorization policy¶
The following example deliberately keeps identity construction outside the policy function. The Principal is assumed to have been created from a verified authentication context and trusted tenant membership.
from dataclasses import dataclass
@dataclass(frozen=True)
class Principal:
subject_id: str
tenant_id: str
permissions: frozenset[str]
@dataclass(frozen=True)
class Order:
tenant_id: str
owner_id: str
def can_access_order(principal: Principal, order: Order, action: str) -> bool:
if principal.tenant_id != order.tenant_id:
return False
if action == "read":
return (
principal.subject_id == order.owner_id
or "orders:read:any" in principal.permissions
)
if action == "refund":
return "orders:refund" in principal.permissions
return False
This is a teaching example, not a complete authorization framework. The refund permission is still tenant-scoped, unknown actions are denied, and the function does not decide whether an order is in a refundable business state. That state transition belongs in the domain operation and should be enforced atomically with the mutation.
The exact implementation and seven unit tests are maintained in interview-handbook/examples/python/api_authz/; the handbook CI runs them on every pull request.
Run from the repository root:
The tests cover owner access, same-tenant support access, cross-tenant denial, refund permissions, and unknown-action denial.
8. Production failure modes and observability¶
| Failure mode | Why it happens | Better control |
|---|---|---|
| IDOR / BOLA | The API checks that a token is valid but not whether the caller may access this object | Object-level authorization and tenant-scoped queries |
| Privilege escalation through mass assignment | Client-controlled fields are copied into a privileged model | DTO allowlists and property-level authorization |
| JWT accepted by the wrong API | Signature is checked but issuer, audience, or token profile is not | Strict issuer, audience, algorithm, expiry, and type validation |
| Stale permissions | Long-lived tokens contain roles that have since been revoked | Short token lifetime, introspection, or explicit revocation |
| Cross-tenant data leak | Tenant ID comes from a request parameter or pooled connection state | Derive tenant from trusted identity; test isolation across requests |
| CSRF against cookie sessions | Browser automatically attaches session cookies | CSRF tokens, Origin checks, and deliberate SameSite policy |
| Credential theft through logs | Authorization or cookie headers are recorded | Structured redaction at proxies, application logs, and traces |
| Authentication denial of service | Aggressive account lockout or expensive password checks are unbounded | Rate limits, adaptive controls, and capacity-aware password hashing |
| Breaking API rollout | New validation or enum values reject older clients | Contract tests, usage telemetry, deprecation windows, and staged rollout |
Log the decision outcome and useful context such as policy identifier, route, tenant identifier, and denial category. Do not log raw bearer tokens, session cookies, passwords, or full sensitive request bodies. Keep authentication failures, authorization denials, validation errors, and business conflicts as distinct metrics; combining them hides both attacks and ordinary client defects.
9. Security and compatibility test checklist¶
Before shipping a protected endpoint, test at least:
- missing, malformed, expired, not-yet-valid, and wrongly signed tokens;
- wrong issuer, wrong audience, unsupported algorithm, and wrong token type;
- an authenticated user without the required permission;
- a user reading, changing, exporting, and deleting another user's object;
- a valid user attempting to cross tenant boundaries;
- a request that adds privileged fields not present in the documented schema;
- role or membership revocation while a session or token is still active;
- cookie-authenticated state-changing requests with invalid CSRF or Origin data;
- oversized bodies, deeply nested JSON, oversized arrays, invalid enums, and boundary values;
- old client payloads against the new API version;
- concurrent state transitions such as two refund requests arriving together.
Test both the policy function and the full HTTP-to-database path. Unit tests prove individual rules; integration tests catch missing middleware, incorrect tenant propagation, route omissions, and database isolation mistakes.
10. Interview questions¶
-
What is the difference between authentication and authorization?
Authentication establishes the principal's identity. Authorization decides whether that principal may perform a particular action on a resource under the current policy. -
Is OAuth 2.0 a login protocol?
OAuth 2.0 is an authorization framework. OpenID Connect adds an identity layer for user authentication and defines ID Tokens. -
Why should an API not accept an OIDC ID Token as an access token?
The ID Token is issued for the client to establish an authentication event. The API is a resource server and must validate an access token intended for that API, including its audience and token profile. -
Does a valid JWT signature mean the request is authorized?
No. The API must validate issuer, audience, expiry, algorithm, token type, and required claims, then make a separate authorization decision for the requested resource and action. -
What is broken object-level authorization?
It occurs when an API uses a caller-controlled object identifier without checking whether the authenticated principal may access that particular object. A valid token or broad scope is not enough. -
RBAC versus ABAC: when would you use each?
RBAC is effective for stable job-function permissions. ABAC handles context such as tenant, region, resource state, and classification. Many systems combine them; policy complexity and explainability must be managed. -
How would you prevent cross-tenant data access?
Derive tenant membership from a trusted identity context, enforce tenant scope in data-access queries, apply object-level policy, and test connection-pool and cache isolation. Database row-level security can provide additional defense in depth. -
When should an API return 401 versus 403?
Use 401 when valid authentication credentials are absent or invalid, with an applicable WWW-Authenticate challenge. Use 403 when the credentials are valid but the operation is not permitted. A consistent 404 policy can conceal resource existence. -
Why might an opaque token be preferable to a JWT?
Opaque tokens can centralize introspection and revocation. JWTs can reduce online validation calls but introduce claim freshness, key rotation, and revocation trade-offs. -
What makes an API change breaking?
Removing or renaming fields, changing semantics or defaults, tightening accepted requests, changing ordering or error behavior, and adding enum values that old clients cannot handle can all break consumers. Compatibility must be tested against real client assumptions. -
How would you retire an old API version safely?
Publish a migration path, signal deprecation, measure remaining usage, provide an overlap period, coordinate important consumers, and only then enforce the published retirement plan. -
Why is request validation not an authorization control?
Validation establishes that input is well-formed and meaningful. It does not establish that the caller is allowed to reference the supplied account, tenant, object, or property.
References¶
Standards and specifications¶
- RFC 9700 — Best Current Practice for OAuth 2.0 Security
- OpenID Connect Core 1.0
- RFC 8725 — JSON Web Token Best Current Practices
- RFC 6750 — OAuth 2.0 Bearer Token Usage
- RFC 9110 — HTTP Semantics
- RFC 9457 — Problem Details for HTTP APIs
- RFC 9745 — The Deprecation HTTP Response Header Field
- RFC 8594 — The Sunset HTTP Header Field