Lesson 93 of 129
Authentication: OAuth 2.0, JWT, and Sessions
Authentication (authn) establishes who the caller is. Authorization (authz) decides what that caller may do. Authentication produces an identity, and authorization uses it together with policy to allow or deny each action. This article covers how a client proves identity and keeps proving it across requests.
HTTP is stateless, so after a user logs in, every later request must carry something that identifies the authenticated user. There are two approaches: a session identifier that references server-side state, or a self-contained token.
Sessions
In session-based authentication, the server creates a session record after login and returns a random session ID, usually in a cookie.
sequenceDiagram
participant B as Browser
participant S as Server
participant DB as Session store
B->>S: POST /login (credentials)
S->>DB: Create session record with user and expiry
S-->>B: Set-Cookie: session_id (HttpOnly, Secure)
B->>S: GET /orders (cookie)
S->>DB: Look up session_id
DB-->>S: user
S-->>B: Response- The session ID must be unguessable (long, random, from a cryptographic generator).
- Revocation is immediate: delete the server-side record.
- Every request needs a lookup. With many servers, the session store must be shared (Redis is common) or requests must be routed to the same server (sticky sessions), which weakens load balancing.
- Cookies are sent automatically by browsers, which makes CSRF a concern (see encryption and attack mitigation). Cookie attributes
HttpOnly(not readable by JavaScript),Secure(HTTPS only), andSameSitereduce the risk.
Tokens and JWT
In token-based authentication, the server issues a signed token that contains the user's identity. The server verifies the signature instead of looking up state.
A JSON Web Token (JWT) has three Base64URL-encoded parts separated by dots: header.payload.signature.
| Part | Content |
|---|---|
| Header | Signing algorithm (alg) and token type, and often a key ID (kid) |
| Payload | Claims: statements about the subject |
| Signature | Signature over header.payload using a secret or private key |
Registered claims include iss (issuer), sub (subject, the user ID), aud (audience, the intended recipient), exp (expiration time), nbf (not before), iat (issued at), and jti (unique token ID).
For example, a decoded payload looks like this:
{"iss": "https://auth.example.com", "sub": "user-42", "aud": "orders-api", "exp": 1790000000, "scope": "orders:read"}The payload is encoded, not encrypted. Anyone holding the token can read it. Do not put secrets in it.
Signing algorithms
- HS256 (HMAC with SHA-256): symmetric. The same secret signs and verifies, so every verifier can also forge tokens.
- RS256 / ES256 (RSA or ECDSA signatures): asymmetric. The issuer signs with a private key. Services verify with the public key, often fetched from a JWKS (JSON Web Key Set) endpoint. Verifiers cannot forge tokens. This suits microservices.
Validation
A service accepting a JWT must:
- Verify the signature with the expected algorithm and key. Reject
alg: none, and do not let the token's header choose the algorithm freely. - Check
exp(andnbf), allowing a small clock skew. - Check
issmatches the trusted issuer. - Check
audincludes this service. - Check authorization: scopes or roles for the requested action.
Revocation
A signed token stays valid until it expires, and the verifier does not consult a server. This is what makes verification scalable, and it also makes revocation hard. Approaches:
- Short lifetimes for access tokens (minutes), with a refresh token to obtain new ones. Revoking the refresh token stops renewal, so exposure is limited to the access token's remaining life.
- Denylist of revoked
jtivalues checked on each request. This reintroduces a lookup, though only against a small set. - Refresh token rotation: each use issues a new refresh token and invalidates the old one. Reuse of an old token signals theft.
Where to store tokens in a browser
A token in localStorage is readable by any script on the page, so an XSS flaw exposes it. A token in an HttpOnly cookie cannot be read by scripts but is sent automatically, so CSRF defenses apply. Neither option removes the need to prevent XSS.
An alternative to self-contained tokens is an opaque access token, a random string that the resource server validates by calling the authorization server's introspection endpoint (RFC 7662). It restores immediate revocation at the cost of a network call per validation, which is usually reduced by caching.
Sessions or tokens
| Server-side sessions | JWT access tokens | |
|---|---|---|
| State | On the server | In the token |
| Revocation | Immediate | Delayed until expiry, or needs a denylist |
| Scaling | Needs shared store | Stateless verification |
| Size per request | Small ID | Larger token |
| Cross-service use | Requires shared store | Any service with the public key verifies |
Sessions fit a single web application with a browser client. Tokens fit APIs and service-to-service calls where many services must verify identity independently.
OAuth 2.0
OAuth 2.0 is a framework for delegated authorization: it lets an application obtain limited access to a user's resources on another service without receiving the user's password. It defines four roles:
- Resource owner: the user.
- Client: the application requesting access.
- Authorization server: authenticates the user and issues tokens.
- Resource server: the API that accepts access tokens.
The client receives an access token, restricted by scopes (for example read:calendar), and optionally a refresh token.
Authorization code flow with PKCE
This is the recommended flow for browser, mobile, and server-side applications acting for a user. PKCE (Proof Key for Code Exchange, pronounced "pixy") binds the authorization request to the token request so that an intercepted authorization code is useless to an attacker. It is essential for public clients (those that cannot keep a secret, such as mobile and single-page apps) and recommended for all clients. The challenge is sent with code_challenge_method=S256, meaning the SHA-256 hash of the verifier, Base64URL-encoded.
sequenceDiagram
participant U as User (browser)
participant C as Client app
participant A as Authorization server
participant R as Resource server
C->>C: Generate code_verifier, code_challenge = SHA-256(verifier)
C->>U: Redirect to /authorize (client_id, scope, state, code_challenge)
U->>A: Authenticate and consent
A-->>U: Redirect to client with code and state
U->>C: code and state
C->>A: POST /token (code, code_verifier)
A->>A: Verify SHA-256(verifier) equals stored challenge
A-->>C: access token (and refresh token)
C->>R: API request with Bearer access token
R-->>C: ResponseSteps:
- The client creates a random
code_verifierand sends its hash (code_challenge) in the authorization request, with a randomstatevalue. - The user authenticates at the authorization server, which never exposes credentials to the client.
- The server redirects back with a short-lived, single-use authorization code.
- The client checks
statematches (protecting against CSRF), then exchanges the code plus the originalcode_verifierfor tokens over a direct back-channel request. - The server verifies the verifier against the challenge, then issues tokens.
The tokens travel on the back channel, not in the browser's URL. The legacy implicit flow returned tokens in the redirect and is no longer recommended.
Client credentials flow
Used for machine-to-machine calls where no user is involved. The client authenticates to the authorization server with its own credentials (client ID and secret, or a signed assertion or mTLS certificate) and receives an access token representing the client itself. Suited to service-to-service calls and batch jobs.
Other points
- Refresh tokens let a client get new access tokens without user interaction and should be stored securely and rotated.
- The resource owner password flow, where the client collects the user's password, is deprecated.
- OAuth 2.0 itself does not define user identity. That is the role of OpenID Connect.
OpenID Connect
OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0. It adds:
- An ID token, a JWT that states who authenticated:
sub(user identifier),iss,aud(the client ID),exp, and optionally the authentication time (auth_time). The client validates it. It is meant for the client and is not sent to APIs as an access token. - An
openidscope that requests it, plus scopes such asprofileandemail. - A UserInfo endpoint for further claims.
- A discovery document at
/.well-known/openid-configurationthat lists endpoints and the JWKS location.
OAuth 2.0 answers "may this client access this resource", and OIDC adds "who is the user".
Single sign-on
Single sign-on (SSO) lets a user authenticate once with an identity provider (IdP) and access several applications without logging in again. Each application redirects to the IdP. If the user already has a session at the IdP, it issues the assertion without prompting.
- Protocols: OIDC (JSON/JWT-based, common for modern apps) and SAML 2.0 (XML-based, common in enterprise).
- Applications trust the IdP and hold no passwords.
- Benefits: central credential policy, multi-factor authentication in one place, and central deprovisioning.
- Risk: the IdP is a critical dependency and an attractive target.
In a microservices system
The gateway commonly validates the user's token and forwards identity to services. Services can verify the token again, or trust a signed internal identity, and authorize on the data they own. Service-to-service calls use client credentials or mutual TLS. See microservices security and zero trust.
Interview questions
- How do you revoke a JWT before it expires?
Not directly, because verifiers do not check state. Use short-lived access tokens with revocable refresh tokens, or keep a denylist of
jtivalues that verifiers check. - Why is PKCE needed in the authorization code flow?
An attacker who intercepts the authorization code (for example via a mobile app's redirect) cannot exchange it without the
code_verifierthat only the legitimate client holds. - What is the difference between OAuth 2.0 and OIDC? OAuth 2.0 delegates authorization and issues access tokens for APIs. OIDC builds on it to authenticate the user and issue an ID token that carries identity claims.
- Why is
alg: noneor algorithm confusion dangerous? If a verifier accepts the algorithm named in the token, an attacker can choosenone(no signature) or trick an RS256 verifier into treating the public key as an HMAC secret. Verifiers must enforce an expected algorithm. - When would you use the client credentials flow? When a service or job calls an API on its own behalf with no user context, such as a backend service calling another service.
