Practice this topic in a realistic system design interview
Once a system grows past a single server, every service that receives a request needs a way to know who the caller is. Ideally, it can do that without calling the login service on every request. A JSON Web Token (JWT) is one common answer: a small token format used to carry facts about a user, client, or service.
A JWT is a container, not a complete login system. Protocols such as OAuth 2.0 and OpenID Connect decide when tokens are issued, who should receive them, and how refresh works. A signed JWT proves the contents were not changed after signing; it does not automatically mean the API should trust it.
JWTs are useful when many services need to validate the same token without calling back to the issuer each time. The trade-off is revocation. Once a signed token is issued, it may be accepted until it expires unless you add a server-side check to reject it earlier.
This chapter explains how a JWT is signed, validated, stored, and revoked.
A JWT is a compact token that carries claims and is usually protected by a digital signature.
Loading simulation...
A claim is a fact written inside the token. A token might say that user 123 is the caller, that https://auth.example.com issued the token, that only orders-api should accept it, that it expires at a specific time, and that it allows orders:read.
Most JWTs used as access tokens are signed. A signed JWT is called a JWS: JSON Web Signature. Signing protects integrity. In plain English, it lets the API detect whether someone changed the token after it was issued.
Signing does not hide the data. The payload is Base64Url-encoded, not encrypted. Anyone who gets the token can decode and read its claims.
Encrypted JWTs also exist. They are called JWE: JSON Web Encryption. They are less common for API access tokens because they add more key-management work. Most systems use signed JWTs and avoid putting sensitive data inside them.
JWTs are useful when several services need to validate the same access token, especially when the login or authorization server is separate from the APIs.
They help when APIs need to validate tokens locally for speed or availability, when tokens must carry issuer, audience, expiry, scopes, or tenant context, and when external clients call APIs using tokens issued through OAuth 2.0 or OIDC.
They are not automatically better than sessions. A server-side session is often the right answer for a first-party browser app because the server can revoke it immediately and keep the client credential small.
The practical question is: who needs to validate the token, and how quickly must you be able to revoke it?
A JWT has three parts separated by dots:
Expanding each of those three parts shows what they hold:
Each part has a separate role.
The header describes how the token was signed.
Common header fields:
| Field | Meaning |
|---|---|
alg | Signing algorithm, such as RS256, ES256, or HS256 |
typ | Token type, commonly JWT |
kid | Key ID used to find the right verification key |
Do not trust the header blindly. The API must allow only the algorithms and keys it expects. The header can help pick a key, but it must not decide the security rules.
The payload contains claims, which are facts the issuer is stating.
Registered claims are common claim names with agreed meanings:
| Claim | Meaning |
|---|---|
iss | Issuer: who issued the token |
sub | Subject: who or what the token represents |
aud | Audience: which service should accept the token |
exp | Expiration time |
nbf | Not valid before |
iat | Issued at |
jti | Token ID |
Applications can also define their own claims, such as tenant_id, scope, or roles.
Example payload:
The payload is visible to anyone holding the token. Do not put passwords, API keys, payment data, recovery codes, or sensitive personal data in JWT claims.
Also avoid putting fast-changing permissions in long-lived JWTs. If a user's role changes from admin to member, an old token with admin may remain valid until it expires unless you check current server-side state.
The signature protects the header and payload from being changed silently.
Conceptually, the signer computes:
The API recomputes the signature input and checks it with a trusted verification key.
There are two common key models:
| Model | Example algorithms | How it works | Good fit |
|---|---|---|---|
| Symmetric | HS256 | Same secret signs and verifies | One issuer and a small trusted set of verifiers |
| Asymmetric | RS256, ES256, EdDSA | Private key signs, public key verifies | Many services validating tokens from one issuer |
Asymmetric signing is common in OAuth and OIDC systems because APIs can verify tokens using public keys without receiving the issuer's private signing key.
JWTs often appear in an access-token flow. The idea is simple: the auth server issues the token, and the API checks it before serving the request.
The user signs in through an authorization server or identity provider.
In modern OAuth/OIDC flows, the application usually does not handle the user's password directly. It redirects the user to the identity provider and receives tokens after sign-in succeeds.
After sign-in and policy checks, the issuer creates a short-lived access token.
Example in application code:
In a production identity system, an authorization server usually creates tokens, not each application service.
The client includes the access token on API requests.
The token is a bearer credential. Whoever has it can use it until it expires or the server rejects it.
Before trusting the token, the API should validate the signature and confirm the signing algorithm is one it allows.
It should check the expected issuer (iss) and audience (aud), the expiration (exp), and the not-before time (nbf) when present. It should also enforce the required scopes or permissions, the tenant or account boundary in a multi-tenant system, and the key ID (kid) against a trusted key set when one is present.
If validation fails, return 401 Unauthorized. If the token is valid but does not grant enough permission for the operation, return 403 Forbidden.
JWT systems fail when key management is treated as an afterthought.
With HS256, the same secret signs and verifies tokens.
That means every service that can validate a token can also create one if it has the secret. This can be acceptable inside a small, tightly controlled system. It is a poor fit when many services, teams, or third parties need to verify tokens.
If you use symmetric keys, generate strong random secrets, store them in a secrets manager, rotate them on a schedule, avoid sharing them broadly, and use separate secrets for separate environments and issuers.
With RS256, ES256, or EdDSA, the issuer signs with a private key. APIs verify with a public key.
Large systems usually publish public verification keys through a JWKS endpoint. Think of JWKS as a URL where APIs can fetch the issuer's public keys.
The token header includes a kid. The API uses that key ID to select the matching public key from the trusted JWKS.
Good key rotation usually has overlap:
Do not fetch arbitrary JWKS URLs from token contents. The issuer and JWKS location must come from trusted server configuration.
JWTs can be used for different token types. Mixing them up creates security bugs.
| Token | Who consumes it | Purpose |
|---|---|---|
| Access token | Resource server or API | Grants API access |
| ID token | Client application | Tells the client who authenticated |
| Refresh token | Authorization server | Gets new access tokens |
An ID token from OpenID Connect is not an API access token. It tells the client who signed in. APIs should not accept ID tokens as proof that the caller can access protected endpoints.
A refresh token should not be sent to normal APIs. It should be sent only to the authorization server's token endpoint. Refresh tokens usually need rotation, revocation, reuse detection, and stronger storage controls than access tokens.
An access token should be short-lived, limited to the API it calls, and scoped to the permissions it needs.
Token storage depends on the client type.
Browser token storage is a trade-off between XSS, CSRF, usability, and architecture. XSS means attacker-controlled JavaScript runs in your page. CSRF means a browser sends a cookie-backed request the user did not intend.
Common options:
| Storage | Main risk | Notes |
|---|---|---|
localStorage / sessionStorage | JavaScript can read tokens after XSS | Avoid for high-value credentials |
| In-memory JavaScript variable | Lost on refresh, still usable by injected scripts | Better than storage that JavaScript can read later |
HttpOnly cookie | Sent automatically, so CSRF must be handled | Protects token from direct JavaScript reads |
| Backend-for-frontend session | Requires server-side state | Strong choice for first-party web apps |
For many first-party web applications, an HttpOnly, Secure, SameSite cookie carrying an opaque session ID is simpler and safer than putting JWTs in browser storage. Opaque means the client cannot read meaning from it; the server looks it up.
If a browser app uses tokens, keep access tokens short-lived, prefer refresh tokens in HttpOnly, Secure, SameSite cookies, use CSRF protection for cookie-authenticated state-changing requests, avoid long-lived JavaScript-readable storage for refresh tokens, and use a strong Content Security Policy to reduce XSS risk.
Use platform secure storage: iOS Keychain, Android Keystore-backed storage, or OS credential vaults for desktop apps. Do not store long-lived tokens in plain files, logs, crash reports, analytics events, or screenshots.
Backend services should avoid hardcoded long-lived JWTs. Prefer cloud or platform workload identity, short-lived service tokens, mTLS or signed service-to-service credentials, and secrets stored in a managed secrets system.
The hardest operational problem with JWTs is revocation, which means making a token stop working before its expiration time.
A self-contained JWT can be validated without contacting the issuer. That is useful for speed and availability, but it means the issuer cannot automatically pull the token back after it leaves.
Common strategies:
| Strategy | How it works | Trade-off |
|---|---|---|
| Short access-token lifetime | Token expires quickly, such as 5-15 minutes | Stolen token works until expiry |
| Refresh token rotation | Each refresh returns a new refresh token | More server-side tracking |
| Revocation list | Store revoked jti or session IDs | Adds a server-side lookup |
| Token introspection | API asks issuer whether token is active | Adds latency and depends on issuer |
| Session version claim | Token includes a user/session version checked by API | Requires lookup for sensitive operations |
Short-lived access tokens plus refresh-token rotation are common. For high-risk actions, many systems still check server-side state even if the access token is a valid JWT. That extra check is typical when a user changes payout details, exports sensitive data, disables MFA, creates API keys, or performs administrative actions.
Stateless validation is a performance optimization. It should not stop the system from making live authorization checks when the risk requires them.
JWTs are easy to parse and easy to validate incorrectly. A safe implementation is strict.
Always validate the signature using a trusted key and an algorithm fixed in server configuration. Check iss, aud, and exp, along with nbf when it is present. Then enforce the required scopes, roles, or permissions and the tenant or organization boundary.
Do not accept a token only because the signature is valid. A valid token for billing-api should not be accepted by orders-api.
Never let the token choose the algorithm policy.
Configure acceptable algorithms explicitly, such as:
Reject alg: "none" and reject unexpected algorithm families. Algorithm confusion bugs happen when an API accepts a token signed with a different algorithm than the service intended.
The classic version of this attack is RS256-to-HS256 confusion. The issuer signs tokens with an RSA private key and publishes the matching public key. The API reads alg from the token header. An attacker re-signs a forged token with alg: "HS256", using the issuer's public key as the HMAC secret.
If the API blindly trusts the header and uses the same key, the forged token may be accepted. The fix is to configure acceptable algorithms on the server and choose the verification key based on that configuration, not on the token header alone.
exp and nbfIssuer and API clocks can drift. A token issued near its exp time can look expired to an API whose clock is a few seconds ahead, even when nothing is wrong.
Production APIs usually allow a small leeway, often 30 to 60 seconds, for exp and nbf. Larger leeway weakens the freshness of short-lived access tokens, so do not stretch it into minutes.
jtiA bearer token can be reused by anyone who captures it. Signature checks alone do not stop replay.
For high-value or single-use tokens, include a unique jti, cache seen jti values until the token's exp, and reject duplicates. This makes the token usable only once at that API. For ordinary short-lived access tokens, teams usually rely on short lifetimes and audience binding; full jti tracking is usually reserved for sensitive operations.
JWTs are sent on every request. Large tokens increase request size, logging risk, gateway header limits, and latency.
Good claims identify the subject, name the issuer and audience, expire the token, carry stable scopes or broad permissions, and add tenant or account context when needed. Poor claims try to stuff in a full user profile, secrets, credentials, large permission graphs, data that changes every few seconds, or sensitive business and personal data.
Roles in a JWT are snapshots. They may be stale.
For broad access checks, a scope or role claim can be fine. For sensitive decisions, check current server-side authorization state.
For example, a JWT may say the user has admin. Before deleting a production project, the API may still check the current membership table, account status, MFA freshness, or risk policy.
JWTs leak through the obvious places: authorization headers, reverse proxy logs, application error logs, browser console output, analytics events, and crash reports. Treat them as secrets, redact them before logging, and remember that a signed JWT is still a bearer credential.
Production, staging, and development should use different issuers, audiences, and signing keys.
Do not let staging tokens work against production APIs. Do not use production identity provider keys in local development.
Avoid these mistakes:
aud validation: Tokens meant for one API can be replayed to another.alg header: The server must enforce allowed algorithms.kid, and no overlap window cause outages or security gaps.JWT is a token format, not a complete authentication system. It is useful when APIs need to validate signed claims locally, especially in OAuth and OIDC-based systems. Its main benefits are small tokens, local verification, and clear token fields such as issuer, audience, expiry, and scope.
The main risks are stale authorization, token theft, poor storage, weak validation, bad key management, and confusion between token types.
Use JWTs when their trade-offs fit the system. Keep access tokens short-lived, validate issuer and audience, allowlist algorithms, rotate keys, avoid sensitive claims, and add revocation or live authorization checks where the business risk requires it.
10 quizzes