Filter concepts by levelShowing all levels.

System Design · Authentication and Authorization

Authentication and Authorization

Who is calling, how they prove it, and what that proof does and does not permit

  • Authn vs authzwho you are vs what you may do — two checks, not one
  • Sessions vs JWTsstateful + revocable vs stateless + scalable
  • OAuth 2.0 + OIDCdelegated access, and the identity layer on top
  • Object-level checksrole or policy alone never confirms resource ownership

1.Authentication vs authorization: who is the caller, what can they do

Authentication proves who is making a request — a login, a token, a certificate. Authorization decides what that already-proven identity is allowed to do. A system can get the first right and still be broken if it never checks the second.

Key features

  • Authentication answers "who are you" — login, session, token, certificate, API key are all authentication mechanisms
  • Authorization answers "what are you allowed to do" — a role, a permission, an ownership check, evaluated after identity is already known
  • Authorization always runs after authentication logically, even when both happen inside one middleware call — you cannot decide what an unknown caller may do
  • A request can be fully authenticated (a valid, unexpired token from a real user) and still be correctly rejected by authorization (that real user has no permission for this specific action)
  • Mixing the two up is a common source of security bugs: code that checks "is this token valid" and treats that as equivalent to "is this action allowed" skips the authorization step entirely

Authentication and authorization compared

Authentication and authorization compared
PropertyAuthenticationAuthorization
Question answeredWho is making this request?What is this identity allowed to do?
RunsFirst, once per session or requestAfter authentication, per action or resource
Typical mechanismPassword + session, JWT, OAuth token, mTLS certificateRBAC role check, ABAC policy, ownership check
Failure response401 Unauthorized (you are not who you claim, or not logged in)403 Forbidden (we know who you are; you still cannot do this)
Example questionIs this really user_id 482?Can user_id 482 delete invoice_id 900?
Two separate checks on one request
Client
API
Auth check
Authz check
  1. 1Request + credentials/token
  2. 2Who is this? (authentication)
  3. 3user_id 482 (identity confirmed)
  4. 4Can 482 delete invoice 900? (authorization)
  5. 5No — not the owner
  6. 6403 Forbidden
  1. Client → API: Request + credentials/token
  2. API → Auth check: Who is this? (authentication)
  3. Auth check → API: user_id 482 (identity confirmed)
  4. API → Authz check: Can 482 delete invoice 900? (authorization)
  5. Authz check → API: No — not the owner
  6. API → Client: 403 Forbidden

Command

text
# Authentication: establishes identity
user = authenticate(request.token)   # -> User(id=482) or reject (401)

# Authorization: checks what that identity may do
if not authorize(user, action='delete', resource=invoice):
    return 403  # known caller, disallowed action

Authentication proves identity (who); authorization decides permission (what they can do with that identity). They are two separate checks, not one — a valid token only ever answers the first question.

Session-cookie auth stores identity server-side in a session store and hands the browser only an opaque ID in a cookie. Token auth (typically a JWT) packs identity and claims directly into a signed token the client holds and resends. The trade-off is stateful-and-revocable versus stateless-and-scalable.

Key features

  • Session-cookie auth: server stores session state (usually in Redis or a database) keyed by a session ID; the cookie holds only that ID, which is meaningless without the server-side lookup
  • Token auth (JWT): the token itself contains the claims (user ID, roles, expiry) and a signature; any service holding the signing key or public key can verify it without a database round trip
  • Revocation is the sharpest trade-off: deleting a session server-side takes effect on the next request; a JWT is valid until it expires no matter what the server "wants" — revoking one early needs an extra denylist, defeating some of the statelessness
  • Cookies are automatically sent by the browser on same-site requests and can be marked HttpOnly (unreadable by JavaScript) and Secure (HTTPS-only), which blocks a large class of token-theft-via-XSS that a JWT stored in localStorage does not get for free
  • Token auth scales horizontally without a shared session store, which is why it dominates for stateless APIs and service-to-service calls, while session cookies remain common for traditional server-rendered web apps talking to one backend

Session/cookie auth vs JWT token auth

Session/cookie auth vs JWT token auth
PropertySession + cookieJWT (token)
Where identity livesServer-side store (Redis/DB), keyed by session IDInside the token itself, as signed claims
What the client holdsAn opaque session ID (meaningless alone)The full signed token (readable, not secret unless encrypted)
Server lookup per requestYes — session store hit to resolve the IDNo — signature verification only, no store hit
RevocationImmediate — delete the server-side recordNot until expiry, unless a denylist is added back in
Scales across services without shared stateNo — needs a shared session storeYes — any service with the verification key can check it
Stateful session vs stateless token

Session + cookie

  • Server holds the source of truth
  • Instant revocation by deleting the record
  • Every request needs a session-store lookup
  • Needs a shared store across instances

JWT (token)

  • Client holds the source of truth (signed)
  • No lookup needed — signature check only
  • Cannot be revoked early without a denylist
  • Scales across services with no shared store
  • Session + cookie
    • Server holds the source of truth
    • Instant revocation by deleting the record
    • Every request needs a session-store lookup
    • Needs a shared store across instances
  • JWT (token)
    • Client holds the source of truth (signed)
    • No lookup needed — signature check only
    • Cannot be revoked early without a denylist
    • Scales across services with no shared store

Command

text
# Session/cookie: cookie carries only an opaque ID
Set-Cookie: sid=9f8c...; HttpOnly; Secure; SameSite=Lax
# server: session_store[sid] -> { user_id: 482, roles: [...] }

# Token (JWT): client carries the claims themselves
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0ODIi...
# decoded payload: { "sub": "482", "roles": ["admin"], "exp": 1893456000 }

Session cookies keep identity server-side (instantly revocable, needs a shared store); JWTs keep it in a signed, client-held token (no lookup needed, but not revocable before expiry without adding a denylist back in).

3.OAuth 2.0 and OpenID Connect: delegated authorization and identity

OAuth 2.0 lets a user grant a third-party app limited access to their data on another service, without sharing their password — it is an authorization protocol. OpenID Connect (OIDC) is a thin identity layer built on top of OAuth 2.0 that adds "and here is who you actually are," which OAuth alone never promises.

Key features

  • OAuth 2.0's core output is an access token scoped to specific permissions ("read this user's calendar") — it was never designed to prove who the user is to the app that receives the token
  • OpenID Connect adds an ID token (a JWT with standardized identity claims: sub, email, name) alongside the OAuth access token — this is what actually authenticates the user to your app
  • "Sign in with Google/GitHub" is OIDC, not plain OAuth — an app using only OAuth's access token to decide who is logged in is misusing a protocol not built for that purpose
  • The authorization code flow (RFC 6749 §4.1) is the standard flow for a server-side app: the browser is redirected to the provider, the provider redirects back with a short-lived code, and the app's backend exchanges that code for tokens directly with the provider — the access token never passes through the browser
  • PKCE (Proof Key for Code Exchange, RFC 7636) is now recommended even for confidential clients, not just mobile/SPA apps — it binds the code exchange to the app that started the flow, closing an interception risk on the redirect step

What OAuth 2.0 and OpenID Connect each actually provide

What OAuth 2.0 and OpenID Connect each actually provide
PropertyOAuth 2.0OpenID Connect (OIDC)
SolvesDelegated authorization — "let this app do X on my behalf"Delegated authentication — "prove who this user is"
Core artifactAccess token (opaque or JWT, scoped to permissions)ID token (always a JWT, standardized identity claims)
Answers "who is this user?"No — not its job, though often misused for thisYes — that is its entire purpose
Built onIts own spec (RFC 6749)OAuth 2.0 — adds the id_token and a userinfo endpoint
Typical use"Let this app read my calendar""Sign in with Google"
OAuth 2.0 authorization code flow (with OIDC id_token)
Browser
App backend
Auth provider
  1. 1Click "Sign in with Provider"
  2. 2Redirect to provider /authorize
  3. 3Login + consent (scope: openid email)
  4. 4Redirect back with ?code=...
  5. 5Deliver code via redirect
  6. 6Exchange code + PKCE verifier for tokens
  7. 7access_token + id_token (JWT)
  1. Browser → App backend: Click "Sign in with Provider"
  2. App backend → Browser: Redirect to provider /authorize
  3. Browser → Auth provider: Login + consent (scope: openid email)
  4. Auth provider → Browser: Redirect back with ?code=...
  5. Browser → App backend: Deliver code via redirect
  6. App backend → Auth provider: Exchange code + PKCE verifier for tokens
  7. Auth provider → App backend: access_token + id_token (JWT)

Command

text
# Authorization code flow, HTTP-shaped

GET https://provider.com/authorize
    ?response_type=code
    &client_id=<app_id>
    &redirect_uri=<callback_url>
    &scope=openid email profile   # "openid" scope -> this is OIDC
    &state=<csrf_token>
    &code_challenge=<pkce_hash>

# ... user logs in and consents at the provider ...
# provider redirects to: <callback_url>?code=<auth_code>&state=<csrf_token>

POST https://provider.com/token
    grant_type=authorization_code
    &code=<auth_code>
    &code_verifier=<pkce_secret>
    &client_id=<app_id>&client_secret=<app_secret>

# response: { "access_token": "...", "id_token": "<jwt>", "expires_in": 3600 }

OAuth 2.0 issues scoped access tokens for delegated authorization; OpenID Connect adds a signed id_token on top for delegated authentication. Read the id_token, verified, to identify a user — never the access token.

4.Machine-to-machine identity: API keys, mTLS and service-to-service auth

When the caller is another service rather than a human at a browser, the mechanisms change: API keys are a simple shared secret sent per request, mTLS authenticates both sides using certificates at the TLS layer itself, and service-to-service identity systems issue short-lived, automatically rotated credentials instead of a long-lived secret anyone can copy.

Key features

  • An API key is a static shared secret sent with each request (a header or query param) — simple to implement, but it does not expire on its own, is often over-shared across a team, and grants whatever it grants to anyone who has the string
  • mTLS (mutual TLS) has both the client and the server present certificates during the TLS handshake, so identity is verified at the connection layer before any application data moves — this is common between services inside a trusted network or mesh
  • Long-lived static credentials (API keys, service account passwords) are a bigger blast radius when leaked than short-lived ones, because there is often no automatic expiry forcing rotation — a leaked key in a log file or a public repo can stay valid indefinitely if nobody notices
  • A service mesh (e.g. Istio) or workload-identity system typically issues short-lived certificates automatically per service instance and rotates them continuously, removing the need for any human-managed secret at all for internal service-to-service calls
  • Choosing between these is about trust boundary, not preference: API keys are common for external third-party integrations where a full mTLS setup is impractical for the caller; mTLS and workload identity dominate for internal service-to-service traffic where both ends are under the same organization's control

Machine-to-machine authentication mechanisms compared

Machine-to-machine authentication mechanisms compared
MechanismCredential lifetimeTypical use
API keyLong-lived (until manually rotated)External third-party API access, simple internal scripts
mTLSCertificate-lifetime (often days to months), rotated by toolingService-to-service calls inside a trusted network or mesh
Workload identity (short-lived cert/token)Minutes to hours, auto-rotatedInternal microservice-to-microservice calls at scale
OAuth 2.0 client credentials grantAccess token expires (often ~1 hour); client secret longer-livedOne service calling another service's API on its own behalf, not a user's
One leak, three blast radii — set by the mechanism, not the response

The same leaked credential. How long it stays useful is decided when the mechanism is chosen, not when the leak is discovered.

  • Three horizontal bars on a shared time axis, all starting at the moment a credential leaks.
  • API key: the bar runs the full width of the axis — a static shared secret stays valid until a human notices and rotates it.
  • mTLS certificate: a much shorter bar — valid for the certificate lifetime, days to months, rotated by tooling.
  • Workload identity: the shortest bar by far — minutes to hours, auto-rotated, so the leak expires on its own.

Command

text
# API key: a shared secret sent per request
GET /api/orders
X-API-Key: sk_live_9f8c2a1b7e4d...

# mTLS: both sides present certificates during the TLS
# handshake itself -- no secret travels in the request body
# (conceptually, both directions verified before HTTP starts)
client_cert = load_cert("service-a.crt")
server_requires_client_cert = True  # server rejects handshake without one

# OAuth 2.0 client credentials grant: service authenticates
# as itself, not on behalf of any user
POST /token
    grant_type=client_credentials
    &client_id=service-a
    &client_secret=<secret>
# response: { "access_token": "...", "expires_in": 3600 }

API keys are simple long-lived shared secrets; mTLS verifies both sides via certificates at the TLS layer; workload/service identity issues short-lived, auto-rotated credentials — the further internal and higher-scale the traffic, the more the shorter-lived option wins.

5.RBAC, ABAC, tenant isolation and object-level authorization

RBAC grants permissions by role ("admins can delete users"). ABAC grants permissions by evaluating attributes of the user, resource and context together ("editors can edit posts they authored, during business hours"). Neither one, by itself, checks whether the caller owns the specific resource being touched — that final check is object-level authorization, and skipping it is one of the most common real-world access-control bugs.

Key features

  • RBAC (Role-Based Access Control) assigns permissions to roles, and roles to users — simple to reason about and audit, but coarse: it answers "can an editor delete posts" as a blanket yes/no, not "can THIS editor delete THIS post"
  • ABAC (Attribute-Based Access Control) evaluates a policy against attributes of the subject (user role, department), the resource (owner, sensitivity, status) and the context (time, IP, device) together — more expressive than RBAC, at the cost of policies that are harder to audit at a glance
  • Tenant isolation ensures one customer/organization's data is never reachable by another tenant, in a multi-tenant system — usually enforced by a tenant ID present on every row and checked on every query, not merely assumed from the URL or a role
  • Object-level authorization is the check that the specific resource instance in THIS request belongs to (or is otherwise permitted for) THIS caller — role or attribute checks alone confirm a category of access, never a specific instance
  • OWASP's API Security Top 10 lists Broken Object Level Authorization as its #1 API risk — because a role check that never verifies resource ownership lets any authenticated user reach any other user's data just by changing an ID in the request, often with no error anywhere in the logs

RBAC, ABAC and object-level authorization compared

RBAC, ABAC and object-level authorization compared
ModelDecides based onWhat it does NOT check by default
RBACThe role(s) assigned to the userWhich specific resource instance is being accessed
ABACAttributes of user + resource + context, evaluated by policyStill needs the resource's attributes (e.g. owner_id) actually fetched and compared — a policy engine only evaluates what it is given
Object-level authorizationDoes THIS user own or have a grant on THIS specific resourceN/A — this is the specific-instance check the other two models leave out unless deliberately added
Tenant isolationDoes this resource belong to the caller's tenant/organization at allRow-level permission within that tenant — a separate, finer-grained check still needed after
Three layers of an authorization decision

RBAC

role grants a category of action

ABAC

policy over user + resource + context

Object-level check

does THIS caller own THIS resource

Allowed

only if every layer passes

  1. RBAC — role grants a category of action
  2. ABAC — policy over user + resource + context
  3. Object-level check — does THIS caller own THIS resource
  4. Allowed — only if every layer passes

Command

text
# RBAC: role -> permission, blanket
if 'editor' in user.roles:
    allow('edit', any_post)   # true for ANY post, not just theirs

# ABAC: policy over attributes
allow = (
    'editor' in user.roles
    and post.author_id == user.id       # resource attribute
    and current_hour in business_hours  # context attribute
)

# Object-level authorization: the specific-instance check
def can_edit(user, post):
    return post.author_id == user.id or 'admin' in user.roles

RBAC checks category (does this role permit this action); ABAC checks policy over attributes; neither checks the specific resource instance by default — object-level authorization (verify THIS caller against THIS resource's ownership) is the check that closes that gap, and OWASP ranks skipping it as the #1 API security risk.

Remember

  • Authentication proves who is calling; authorization decides what they may do — a valid token only ever answers the first question.
  • Session cookies keep identity server-side and revocable; JWTs push it into a signed, client-held token that is valid until it expires, denylist or not.
  • OAuth 2.0 grants scoped access; only OpenID Connect's id_token is meant to prove user identity — reading the access token as identity is a protocol misuse.
  • A role or policy check alone never confirms the caller owns the specific resource in the request — object-level authorization is the check that closes that gap, and OWASP ranks skipping it as the #1 API risk.

Pro tip

  • Prefer short-lived, auto-rotated credentials (mTLS certs, workload identity) over long-lived API keys for internal service-to-service traffic — a leaked short-lived credential resolves itself within its rotation window.
  • Enforce tenant isolation inside every query that touches tenant-owned data, never just in the URL structure or UI — one overlooked endpoint is enough to leak across the boundary.

Next up

  • Security architecture
  • Encryption
  • Threat modeling

Sources

  • RFC 6749 (OAuth 2.0) — datatracker.ietf.org
  • OWASP API Security Top 10 — owasp.org
  • Checked 2026-08-25

Spotted a mistake on this page? Tell us.