How JWTs Work

Intermediate
7 min read· Security

A JSON Web Token (JWT) is a compact, URL-safe token that encodes claims (user ID, roles, expiry) as a signed JSON payload. The server signs the token with a secret key; any server with the same key can verify the signature and trust the claims — without a database lookup. This makes JWTs stateless: your API can validate a token in microseconds just by verifying the signature. They are the most common token format for APIs, OAuth 2.0 access tokens, and authentication systems.

Think of it like a signed government ID

A passport contains your identity claims (name, nationality, birthdate) and is signed by a government. Any border agent can verify it's genuine by checking the signature and security features — they don't need to call a central database for every traveller. A JWT works the same way: it contains claims signed by your server. Any service with the public key can verify the signature and trust the claims instantly, without a round trip to a central auth server.

Step by Step

1 / 6

Key Concepts

Three-Part Structure

A JWT is header.payload.signature. All three parts are base64url-encoded (NOT encrypted — anyone can decode them). base64url({"alg":"RS256","typ":"JWT"}) + "." + base64url({"sub":"123","exp":1234}) + "." + signature. The payload is readable by anyone who has the token — never put sensitive data (passwords, PII) in JWT claims.

Registered Claims

Standardised payload fields: iss (issuer — who created the token), sub (subject — the entity the token represents, usually user ID), aud (audience — intended recipient), exp (expiry Unix timestamp), iat (issued at), nbf (not valid before), jti (JWT ID — unique token identifier for revocation). Always set exp.

HS256 (Symmetric)

HMAC-SHA256. Uses one shared secret to both sign and verify. Simple to implement — all services share the same secret. Problem: every service that needs to verify tokens must know the secret. If one service is compromised, the secret is compromised. Best for single-service or trusted-backend scenarios.

RS256 (Asymmetric)

RSA-SHA256. A private key signs tokens (only the auth server has it). A public key verifies tokens (any service can have it, even third parties). Compromising a verifying service doesn't compromise the signing key. Required for distributed systems where multiple services verify tokens independently. The auth server publishes public keys via a JWKS endpoint.

JWKS (JSON Web Key Set)

A public endpoint (typically /.well-known/jwks.json) that exposes the public keys used to verify tokens. Services fetch JWKS on startup and cache the keys. When a JWT arrives, the service finds the key by kid (key ID in the token header) and verifies the signature. Key rotation: add a new key to JWKS, start signing new tokens with it, retire old keys after old tokens expire.

Stateless vs Stateful Tokens

JWTs are stateless: all claims are in the token, no server-side storage needed. Downside: you can't revoke a JWT before it expires (the server has no record of it). Stateful (opaque) tokens are random strings stored in a database — fully revocable but require a DB lookup on every request. JWTs trade revocability for speed; keep expiry times short (15 minutes) to limit the revocation window.

Token Expiry

The exp claim is a Unix timestamp after which the token is invalid. APIs must reject expired tokens. Short-lived access tokens (15 minutes to 1 hour) limit damage if stolen. Pair with refresh tokens (stored securely) to renew access without re-login. Never issue non-expiring JWTs for user auth.

The "alg: none" Attack

A famous JWT vulnerability: setting alg to "none" in the header signals no signature is needed. Buggy libraries accepted unsigned tokens as valid. Always explicitly specify which algorithms your library should accept (e.g., only RS256) — never allow "none". Use a well-maintained JWT library and always validate the algorithm header.

Key Facts

  • The JWT specification (RFC 7519) was published in 2015. It is now the most widely used token format for API authentication, used by AWS, Google, Microsoft, and virtually every major platform.
  • base64url encoding is NOT encryption. Anyone who has a JWT can decode the header and payload and read all claims. JWTs are signed (tamper-proof) but not encrypted. For encrypted JWTs, use JWE (JSON Web Encryption).
  • A typical JWT is 200–400 bytes. Compared to a session cookie (just a random ID), JWTs are much larger — but avoid a database roundtrip on every request, making them more scalable.
  • The JWT spec defines multiple signing algorithms. RS256 and ES256 (ECDSA) are preferred over HS256 for distributed systems. ES256 produces smaller signatures than RS256 and is faster to verify.
  • Auth0 research found that 68% of JWTs in the wild have no expiry (exp) claim set. Non-expiring JWTs are a significant security risk — a stolen token is valid forever.
  • Cloudflare, Fastly, and Vercel Edge Functions verify JWTs at the CDN edge layer — in sub-millisecond time using the RS256 public key — before requests ever reach origin servers.

Real-World Applications

Stateless API authentication

A mobile app logs in, receives a JWT, and sends it with every API request. The API verifies the signature in <1ms — no database roundtrip. Ten million concurrent users make no difference to the auth layer's performance. This is why JWTs are the default for mobile and SPA backends.

Microservice-to-microservice auth

Service A calls Service B and includes a JWT in the Authorization header. Service B verifies the token using the auth server's public JWKS. Service B knows the caller's identity and roles without a network hop to an auth server. The public key is cached — verification is entirely local.

Passing context between services

JWTs are not just for auth — they carry structured claims. An API Gateway verifies the user's JWT, extracts user ID and tenant ID, and forwards them as claims in a new internal JWT to downstream services. Services trust these forwarded claims without re-verifying against the database.

Email verification and password reset links

A one-time, short-lived JWT (exp: 15 minutes) embedded in email links verifies the user's email or authorises a password reset. The server signs the JWT with the user's ID and action type. When the link is clicked, the server verifies the JWT — no database storage needed. After expiry, the link is invalid.

Frequently Asked Questions

Can I store sensitive data in a JWT?

No. The JWT payload is base64url-encoded, not encrypted — anyone with the token can read it. Only store non-sensitive claims: user ID, roles, email, expiry. Never put passwords, PII (full name, address), financial data, or other sensitive information in JWT claims. If you need to transmit sensitive data, use JWE (JSON Web Encryption) or keep it in the database and fetch it server-side.

How do I revoke a JWT before it expires?

JWTs are inherently non-revocable without additional infrastructure. Options: (1) Keep access tokens short-lived (15 minutes) — the revocation window is small. (2) Maintain a blocklist (Redis set of revoked jti values) — check it on every request, adds latency. (3) Use refresh tokens for revocation — revoke the refresh token so no new access tokens can be issued. Most production systems combine short-lived JWTs with a blocklist for high-value revocation events like logout and password change.

Should I use HS256 or RS256?

RS256 for most production systems. With RS256, only the auth server has the private signing key. Any number of services can verify tokens using the public key — a compromise of one verifying service doesn't let it forge tokens. Use HS256 only if all verifying services are fully trusted and share the same secret (e.g., a monolith with a single secret).

Where should I store JWTs in the browser?

Access tokens: in memory (JavaScript variable) — safest against XSS, lost on refresh. Refresh tokens: in an HttpOnly, Secure, SameSite=Strict cookie — JavaScript cannot read it (XSS-safe), CSRF-safe. Never store JWTs in localStorage — XSS attacks can steal anything in localStorage. The BFF (Backend For Frontend) pattern keeps all tokens server-side: the browser holds only a session cookie.

Related Topics