Authentication vs Authorization
Who you are vs what you can do. The two-word vocabulary every API designer must use precisely.
Summary#
Authentication (AuthN) answers: who are you? It binds the caller to an identity — a user account, a service principal, a device. The mechanisms are passwords, certificates, hardware tokens, single-sign-on assertions, biometrics, and combinations of these (multi-factor authentication).
Authorization (AuthZ) answers: what are you allowed to do? Given an established identity, AuthZ decides whether this caller can perform this operation on this resource. The mechanisms are roles, scopes, attributes, relationships, and policies.
These are two different questions, separated by a clean conceptual line: identity is established once per session; authorization is checked on every request. The professional vocabulary uses both words, never substitutes one for the other, and never describes a system as “doing auth” without saying which half.
The most common API-security failure of the last decade — broken object-level authorization (BOLA) — is the failure to do AuthZ correctly even when AuthN is solid. The second most common — treating an OAuth 2 access token as proof of identity — is the failure to keep AuthN and AuthZ distinct in the design. Both are vocabulary failures before they are code failures.
Why it matters#
Three reasons the AuthN/AuthZ distinction is load-bearing:
- They fail differently and require different fixes. AuthN failures look like “someone got in pretending to be Alice.” AuthZ failures look like “Alice got at Bob’s data.” The root causes are different; the audit trails are different; the regulatory implications are different (a GDPR breach is usually an AuthZ failure). A team that lumps them together cannot diagnose the failure they are about to ship.
- They live in different parts of the request lifecycle. AuthN runs once per session or per token — validate a JWT, check a session cookie, complete an mTLS handshake. AuthZ runs on every operation, often multiple times per request (gateway-level coarse AuthZ, then application-level fine-grained ownership checks). Confusing the layers produces both holes (“we authorised at the gateway, we don’t need to check again”) and waste (“we re-validate the JWT signature on every database query”).
- The OAuth 2 vs OpenID Connect distinction depends on it. OAuth 2 is an authorization protocol — it issues access tokens that say “the bearer of this token can do X.” It does not say who the bearer is. OpenID Connect layers identity on top of OAuth 2 via an
id_token. “Sign in with Google” is OpenID Connect; “Connect your Google Calendar to my app” is OAuth 2. Mixing these is the most common API-security bug in the protocol family.
In an interview, the candidate who says “we’d use OAuth for login” has just failed the vocabulary check. The senior version: “we’d use OpenID Connect — that’s OAuth 2 plus an identity layer — and the API would verify the id_token for AuthN, then check role and ownership for AuthZ.”
How it works#
Authentication — establishing identity#
AuthN binds an HTTP request to an identity. The mechanism varies by context:
- Username + password — the historical default. Hashed and salted server-side (bcrypt / argon2 / scrypt; never plain SHA-256). Always sent over TLS. Almost always paired with MFA in 2026.
- Session cookies — the post-login state. The server stores a session record; the cookie carries an opaque session ID; every subsequent request re-fetches the session. The cookie has
HttpOnly,Secure,SameSite=Laxflags. - Bearer tokens (JWT or opaque) — short-lived strings sent in
Authorization: Bearer <token>. JWTs carry claims (sub, iss, exp, scopes) inside the token, signed by the issuer; opaque tokens are looked up server-side. Issued by an OAuth 2 / OpenID Connect provider. - API keys — long-lived static strings, identifying the calling application, not a user. Used for server-to-server and machine-to-machine calls.
- Mutual TLS — the client presents a certificate during the TLS handshake; the server’s TLS layer verifies the chain and exposes the client’s identity to the application. Dominant pattern in zero-trust service meshes.
- Single sign-on (SSO) — SAML for enterprises, OpenID Connect for everything else. The user authenticates once at an identity provider; downstream apps trust signed assertions from that provider.
- Multi-factor authentication (MFA) — TOTP, push notifications, hardware tokens (WebAuthn / passkeys). Adds a factor that AuthN-via-password alone cannot establish.
A well-designed API typically supports two AuthN paths: one for users (OpenID Connect / session) and one for machines (API keys or mTLS). Mixing the two paths into one entrypoint is a code smell.
Authorization — deciding what’s allowed#
AuthZ runs after AuthN. Given that we know who is calling, the question is what they can do. Four families dominate:
Role-Based Access Control (RBAC)#
Users have roles; roles have permissions; permissions are checked on operations.
User Alice └─ role: admin └─ permissions: [orders.read, orders.write, users.read, users.write]
Operation: DELETE /orders/123 required permission: orders.write → Alice has orders.write → ALLOWCheap, well-understood, ships in every web framework. Limitations: roles get bloated (the “kitchen sink admin”), roles do not naturally express ownership (“Alice can edit her own orders but not Bob’s”), and inheriting roles across organisations gets messy.
Attribute-Based Access Control (ABAC)#
Decisions are functions of attributes — user attributes (department, clearance, tenure), resource attributes (sensitivity, owner, classification), environmental attributes (time of day, source IP).
ALLOW if: subject.department == resource.department AND subject.clearance >= resource.sensitivity AND environment.time within business_hours AND environment.source_ip in allowed_rangesExpressive enough for any policy you can write; hard to audit at scale; tooling (Open Policy Agent, AWS IAM policies) makes it tractable.
Relationship-Based Access Control (ReBAC)#
Permissions follow relationships between objects. Pioneered by Google’s Zanzibar; the model behind Google Drive sharing, GitHub repo permissions, and Figma project membership.
doc:report-q3#viewer@group:finance#member group:finance#member@user:alice
→ Can Alice view doc:report-q3? → Walk the graph: user:alice → group:finance member → doc:report-q3 viewer → YESReBAC is the right model whenever permissions are inherently a graph — shared documents, hierarchical folders, organisational hierarchies. Production implementations: SpiceDB, OpenFGA, Permify.
Scopes (OAuth 2-style)#
Scopes are coarse-grained permission strings carried in an access token: calendar.read, repo.write, admin:org. They tell the resource server “this token is allowed to do these things.” Scopes alone are not enough — they say what surface a caller can hit, not which resources on that surface they own. Scopes need to combine with ownership checks to make BOLA defence work.
Token has scope: calendar.read Request: GET /calendar/events?owner=alice
AuthZ check 1: token has calendar.read scope? → yes AuthZ check 2: caller (= sub claim) == alice? → must checkWhere they meet — and where they get confused#
A complete authenticated and authorized request:
1. AuthN — who are you? Header: Authorization: Bearer eyJ... Server: verify JWT signature → caller = user_a3f9c2 (sub claim)
2. Coarse AuthZ — does this token have any access to this surface? Token scope: calendar.read Endpoint: GET /calendar/events (requires calendar.read scope) → allow into the application
3. Fine AuthZ — does this caller own this resource? Requested resource: calendar event evt_42 Database: owner of evt_42 is user_a3f9c2 → allow
4. Optional — secondary checks Re-authentication for high-stakes operations (admin actions) MFA challenge for sensitive resourcesThe structural failure that produces BOLA is skipping step 3. The token was authenticated; the scope was approved; the application then assumed “you’ve gotten this far, you can read whatever you ask for” and read out the resource without checking ownership. Two examples of the failure mode:
- The implicit-owner anti-pattern.
GET /orders/{id}looks up the order byidalone and returns it. The caller’s identity from step 1 is never compared to the order’scustomer_id. Anyone with a valid token can iterateidvalues and exfiltrate all orders. - The trust-the-client anti-pattern.
GET /orders?customer_id=alicefilters bycustomer_idfrom the query parameter, trusting that the client only ever sends its own ID. The fix: ignore the parameter; use the authenticated caller’s ID from the token.
Worked example — getting it wrong#
Two real-shaped bugs:
Bug 1: Access token as identity.
A team builds a “Sign in with Acme” feature using OAuth 2. After the OAuth flow, the third-party app holds an access token. The team writes:
# Hand the token to our /me endpoint to identify the userresp = acme.get("/v1/me", headers={"Authorization": f"Bearer {access_token}"})user_id = resp.json()["id"]session["user_id"] = user_id # log them inThe bug: an access token is an authorization artefact, not an authentication artefact. The third-party app received the token via the OAuth flow but cannot prove the token was issued for them. An attacker who steals an access token from any other Acme-integrated app can present it here and impersonate that user.
The fix: use OpenID Connect. The id_token is a signed JWT bound to the client_id of the receiving app; it can be verified locally and proves identity. Or, if sticking with OAuth 2, validate the access token at Acme’s introspection endpoint and check the aud (audience) claim matches the receiving app’s client_id.
Bug 2: Session cookie authorising admin action without re-auth.
A user logged in 6 hours ago. The session cookie is still valid. They click “Delete Organisation.” The endpoint authenticates the cookie, finds the user is an admin role on this org, and runs the deletion.
The bug: an admin role grants the capability but a 6-hour-old session is weak evidence the human at the keyboard is the admin. Stolen laptops, shared computers, social-engineering scenarios all match this profile.
The fix: high-stakes operations require step-up authentication. The endpoint checks the session’s auth_time claim; if it’s older than 5 minutes, redirect to re-auth (password + MFA again) before executing. GitHub’s “sudo mode,” AWS’s MFA-required-for-deletion, Google Workspace’s re-auth-for-billing all implement this pattern.
Variants and trade-offs#
The AuthN/AuthZ split shows up at every scale, but the mechanisms shift:
User-facing API. AuthN via OpenID Connect or sessions; multiple factors expected. AuthZ via roles + ownership checks; scopes if third parties are involved. Step-up auth for sensitive actions. Audit log on every write.
Service-to-service API. AuthN via mTLS or signed service tokens (SPIFFE / JWT-SVID). AuthZ via service identity ACLs (“payments-service can call ledger-service”) plus per-call resource checks. No interactive step-up — the calling service either has authority or it doesn’t.
| Dimension | User-facing | Machine-to-machine | Public partner |
|---|---|---|---|
| AuthN mechanism | OIDC / session + MFA | mTLS / signed service token | OAuth 2 client credentials |
| AuthZ mechanism | RBAC + ownership | Service ACL + resource check | Scopes + ownership |
| Token lifetime | Access 1h, refresh 30d | mTLS cert 1h | Access 1h, refresh 90d |
| Step-up auth | For admin actions | N/A | N/A |
| Identity proof | sub claim in id_token | Cert CN or SPIFFE ID | client_id |
| BOLA defence | Per-resource ownership check | Per-resource ACL check | Per-resource ownership check |
When this is asked in interviews#
The AuthN/AuthZ distinction is a single-question test of vocabulary discipline. Watch for these moments:
- “How would you handle authentication?” Answer for AuthN specifically — identity mechanism, token format, MFA. Then proactively segue: “And for authorization, separately, we’d…”
- “How would users sign in with Google?” OpenID Connect, not OAuth 2. Get the protocol right; the interviewer is checking.
- “How do you prevent a user from accessing another user’s data?” Object-level authorization (BOLA defence). Every read endpoint that takes a resource ID enforces
resource.owner_id == authenticated_caller_id. Mention the failure mode by name. - “What about admin actions?” Step-up authentication. Re-authenticate, re-MFA, even if the session is otherwise valid. Audit log everything.
- “How do you do machine-to-machine authentication?” mTLS or OAuth 2 client credentials. Avoid baking long-lived API keys into client code if you can issue short-lived service tokens instead.
The senior-signal phrasing: “Authentication tells me who the caller is; authorization tells me what they can do; I keep them separate in the design and the code.” Said once, with the right vocabulary, it covers half the security round.
Related concepts#
- API Security — An Overview — the five-property model; AuthN and AuthZ are two of the five.
- OAuth 2 — The Authorization Framework — the dominant AuthZ protocol; the four roles, the grant types.
- OpenID Connect and SAML — the AuthN protocols layered on top of OAuth 2 and used in enterprises.
- Transport Layer Security (TLS) — the layer that makes both AuthN and AuthZ tokens safe to send.
- API Security — A High-Level Recap — the consolidated checklist; both halves of access control sit on it.