What is a JWT (JSON Web Token)?

What is a JWT (JSON Web Token)?

A complete, beginner‑friendly guide to how JSON Web Tokens work under the hood — from the history and the problem they solve, to internals, security pitfalls, and how Netflix, Google, and Uber use them in production.

01

Introduction & History

Imagine you visit an amusement park. At the entrance, you pay for your ticket and the staff clips a wristband onto your arm. For the rest of the day, you don’t need to show your receipt or explain who you are at every ride — you just show your wristband. The person checking the ride doesn’t need to call the ticket booth to confirm you paid; the wristband itself proves it, because it has a special hologram that’s hard to fake.

A JWT (JSON Web Token, pronounced “jot”) is the digital version of that wristband. It’s a small, self‑contained piece of text that proves who you are and what you’re allowed to do, without the server having to “call the booth” (check a database) every single time you make a request.

JWT is formally defined in RFC 7519, published by the Internet Engineering Task Force (IETF) in May 2015. But the idea existed earlier — the specification was first drafted around 2010–2011 as part of a broader family of standards being built for modern web and mobile authentication, alongside its siblings:

RFC 7515 — JWS

JSON Web Signature: defines how to cryptographically sign JSON content.

RFC 7516 — JWE

JSON Web Encryption: defines how to encrypt JSON content.

RFC 7517 — JWK

JSON Web Key: a standard format for representing cryptographic keys.

RFC 7518 — JWA

JSON Web Algorithms: the list of approved signing / encryption algorithms.

Before JWTs became popular, the web mostly relied on server‑side sessions. When you logged in, the server created a session record in memory or in a database, and gave your browser a small random ID (a session cookie) that pointed to that record. Every request, the server looked up that ID to figure out who you were.

This worked fine for simple websites, but the 2010s brought a wave of new complexity: mobile apps, single‑page applications (SPAs) built with frameworks like AngularJS and React, and back‑ends split into dozens of independently deployed microservices. Session lookups that used to be a single in‑memory check now had to be a network call to a shared session store, for every single service, on every single request. Companies like Google, Auth0, and later the OAuth working group pushed for a token format that could carry identity information inside itself — cryptographically signed so it couldn’t be tampered with — so that any service, anywhere, could verify a user’s identity without phoning home. JWT was the answer, and it quickly became the backbone of modern authentication standards like OAuth 2.0 and OpenID Connect (OIDC).

2010

First drafts

The JOSE (JavaScript Object Signing and Encryption) working group begins drafting a family of specifications including JWS, JWE, JWK, JWA and JWT.

2012

OAuth 2.0 arrives (RFC 6749)

OAuth 2.0 formalises bearer tokens as the standard way for third‑party apps to call APIs on a user’s behalf — creating the natural home for JWTs to fill.

2014

OpenID Connect 1.0

OIDC layers standardized identity on top of OAuth 2.0 — and mandates that its id_token is always a JWT.

May 2015

RFC 7519 published

JWT becomes an official IETF standard, alongside RFC 7515 (JWS), 7516 (JWE), 7517 (JWK), and 7518 (JWA).

2015–2018

Mass adoption

Auth0, Firebase Authentication, AWS Cognito, and countless microservice fleets standardize on JWT as the default access‑token format.

2020s

Hardening & hybrids

Industry consolidates around short‑lived access JWTs paired with revocable refresh tokens, JWKS‑based rotation, and edge validation at API gateways.

Analogy recap

A session cookie is like a locker number — the server has to open the actual locker (database) to know what’s inside. A JWT is like a sealed, tamper‑evident envelope that already contains a photocopy of your ID — anyone with the right “letter opener” (the secret key) can verify it’s genuine without calling anyone.

02

The Problem & Motivation

To understand why JWT exists, it helps to walk through the pain points of the system it replaced: server‑side sessions.

2.1 · The session bottleneck

In a traditional session‑based system, the server must remember every logged‑in user. That memory has to live somewhere — usually an in‑memory store like Redis, or a database table. Every incoming request forces the server to make a round trip to that store just to answer the question: “Who is this?”

This is fine when you have one server. But real applications run many servers behind a load balancer for redundancy and scale. Now you have a new problem.

2.2 · Statefulness fights horizontal scaling

If Server A creates a session in its own memory and the load balancer routes the user’s next request to Server B, Server B has no idea who this user is — the session doesn’t exist there. There are only a few ways to fix this, and none of them are free:

  • Sticky sessions — the load balancer always routes a user to the same server. This breaks down when that server crashes or needs to be taken offline for a deploy.
  • Centralized session store — all servers share one Redis / DB instance holding session data. This works, but that shared store becomes a single point of failure and an extra network hop on every request.

2.3 · Cross‑domain and cross‑service identity

Modern systems are rarely just “one website, one server.” A single user action — say, opening a food delivery app — might touch an authentication service, an order service, a payments service, and a notifications service, each running independently, sometimes on different domains or subdomains. Passing a raw session ID between all of these means every one of them needs access to the same central session store, tightly coupling services that should be independent.

JWT’s core motivation was simple: what if the proof of identity could travel with the request itself? Instead of a meaningless random ID that only means something when looked up centrally, JWT bundles the actual claims (who you are, your roles, when it expires) into the token, and cryptographically signs it. Any service holding the shared secret or public key can verify it in microseconds, in memory, with zero network calls.

Session‑based pain points

  • Requires centralized / shared session storage
  • Extra network hop per request to validate
  • Harder to scale horizontally without stickiness
  • Awkward across multiple domains / services

What JWT promised

  • Self‑contained, verifiable without a DB lookup
  • Naturally stateless — any server can validate
  • Works cleanly across domains and services
  • Standardized format understood by many libraries
03

Core Concepts

Before diving into the internals, let’s define the vocabulary you’ll see everywhere in JWT‑land, explained in plain English.

3.1 · Claim

A claim is just a piece of information about the user or the token itself, stored as a key‑value pair — like "name": "Asha" or "role": "admin". Think of it as one line on a form: “Name: Asha.”

3.2 · Encoding vs. Encryption vs. Signing

This is the single most misunderstood part of JWT, so let’s be very precise:

TermWhat it meansCan anyone read it?Can anyone change it undetected?
Encoding (Base64URL)Converts JSON into URL‑safe textYes — trivially decodedYes, if unsigned
Signing (JWS — what most JWTs use)Adds a cryptographic signature over the contentYes — content is still readableNo — any change breaks the signature
Encryption (JWE)Actually scrambles the content so it’s unreadableNo — only holder of the key can read itNo
!
Common misconception

A standard JWT is signed, not encrypted. Anyone can copy‑paste your token into a site like jwt.io and read every field inside it. The signature only proves the content hasn’t been tampered with — it does not hide the content. Never put secrets like passwords or credit card numbers inside a JWT payload.

3.3 · Symmetric vs. Asymmetric Signing

There are two families of algorithms used to sign JWTs:

Symmetric (HMAC) — e.g. HS256

One shared secret key both signs and verifies the token. Simple, but every service that verifies tokens must also hold the secret that can create them.

Asymmetric (Public / Private Key) — e.g. RS256, ES256

A private key signs the token; a public key verifies it. The private key stays only on the auth server. Every other service can safely hold just the public key — it can check tokens but can’t forge them.

3.4 · Access Token vs. Refresh Token

In most real systems, you don’t get just one JWT — you get two:

  • Access token — short‑lived (minutes), sent with every API request, proves who you are right now.
  • Refresh token — long‑lived (days / weeks), stored more securely, used only to request a brand‑new access token when the old one expires.

This two‑token pattern balances security (short‑lived tokens limit damage if stolen) with usability (you don’t have to log in again every 15 minutes).

3.5 · Bearer Tokens — “whoever holds it, owns it”

A JWT is typically used as a bearer token, meaning possession alone is proof of authorization — much like a movie ticket doesn’t ask “are you really the person who bought this?” before letting you in. Whoever presents a valid JWT is treated as the authorized user, no additional proof required. This is convenient, but it also means that if a bearer token is stolen (say, through a network sniffing attack or an XSS exploit), the thief can use it exactly as freely as the real owner until it expires. This single fact is the reason so much of JWT security practice — short lifetimes, HTTPS everywhere, secure storage — exists: it’s all about limiting what a thief can do with a stolen bearer token.

3.6 · JWT vs. Opaque Tokens

Not every “token” is a JWT. An opaque token is just a random string (like a8f3e9c1...) that means nothing on its own — the receiving server must call back to the issuing authority to find out what it represents, similar to how the old session‑ID model worked. JWTs are sometimes called “self‑contained” or “transparent” tokens by contrast, because the claims travel inside the token itself.

AspectJWT (self‑contained)Opaque Token
VerificationLocal, cryptographicRequires a call to the issuing server
RevocationHard before expiryInstant — issuer can invalidate anytime
SizeLarger (encodes claims)Small, fixed‑size random string
Best forDistributed, high‑scale systemsSystems needing tight, instant control

Many real systems actually use both: an opaque refresh token (so it can be instantly revoked) paired with a JWT access token (so it can be verified quickly and statelessly) — getting the best properties of each.

04

Architecture & Anatomy of a JWT

A JWT is just a single string made of three Base64URL‑encoded parts, separated by dots:

Shape of a JWT
xxxxx.yyyyy.zzzzz
HEADER.PAYLOAD.SIGNATURE

Here’s a real (example) token, broken into its three parts:

Example JWT string
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFzaGEiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTAwMDAwMDAsImV4cCI6MTcxMDAwMzYwMH0
.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

4.1 · Part one · Header

The header describes the token’s metadata — which algorithm was used to sign it, and its type.

Header JSON
{
  "alg": "HS256",
  "typ": "JWT"
}

4.2 · Part two · Payload (the claims)

This is the actual data. Claims come in three flavors:

Registered Claims

Standardized, optional fields defined by RFC 7519: iss (issuer), sub (subject / user id), aud (audience), exp (expiration), nbf (not before), iat (issued at), jti (unique token ID).

Public Claims

Custom claims meant to be shared openly, ideally registered in the IANA JSON Web Token registry to avoid collisions, e.g. email.

Private Claims

Custom claims agreed upon between specific parties for their own use, e.g. "role": "admin" or "orgId": "acme-42".

Payload JSON
{
  "sub": "1234567890",
  "name": "Asha",
  "role": "admin",
  "iat": 1710000000,
  "exp": 1710003600
}

4.3 · Part three · Signature

The signature is what makes the token trustworthy. It’s computed like this (for HMAC / HS256):

Signature formula
signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secretKey
)

Anyone who has the secretKey can recompute this signature over the header and payload they received, and check whether it matches the signature attached to the token. If even one character of the header or payload changes, the signature will no longer match — that’s the tamper‑detection in action.

4.4 · Designing a good claim set

A well‑designed JWT payload is deliberately minimal. A useful mental exercise when adding a new claim is to ask: “does a receiving service need this to make an authorization decision right now, without a database call?” If the answer is no — if it’s display information like a full name or avatar URL that only the UI needs — it usually belongs in a separate profile lookup, not baked into every single token. Overloaded payloads are a common source of both the size bloat discussed in the Performance section and unnecessary data exposure, since (as a reminder) anything in the payload is readable by anyone holding the token.

A reasonable rule of thumb used by many production teams: keep the payload to under a dozen claims, favor short, well‑known keys, and treat every additional claim as a small, ongoing cost across every request the token is attached to, not a one‑time decision.

Header (JSON) alg · typ Payload (JSON) the claims Base64URL Encode URL‑safe text Base64URL Encode URL‑safe text Header.Payload signing input string sign secret / priv Secret / Priv Key HS256 / RS256 Signature bytes Final JWT  ·  Header.Payload.Signature
Figure 1 · The assembly line: the header and payload are each turned into JSON, then Base64URL‑encoded text, glued together with a dot, and fed into the signing algorithm along with a secret key. The output signature is glued on as the third part.
05

How It Works Internally

Opening the hood: encoding, algorithm families, and the precise verification sequence every server must perform on every incoming token.

5.1 · Base64URL, not Base64

JWT uses Base64URL encoding rather than standard Base64. Standard Base64 uses +, /, and = characters, which have special meaning in URLs (they can break query strings or need escaping). Base64URL swaps + for -, / for _, and drops the trailing = padding — making the token safe to place directly in a URL, HTTP header, or cookie without any extra escaping.

5.2 · Signing algorithms in detail

AlgorithmFamilyHow keys workTypical use
HS256 / HS384 / HS512HMAC (symmetric)One shared secretSingle backend, monolith, internal services
RS256 / RS384 / RS512RSA (asymmetric)Private key signs, public key verifiesMulti‑service systems, OAuth providers
ES256 / ES384 / ES512ECDSA (asymmetric)Private / public elliptic‑curve key pairSame as RSA but smaller, faster keys
noneNo signature at allNever use in production
!
The “alg: none” attack

Early JWT libraries trusted the alg field written inside the token itself. An attacker could set alg to none, remove the signature entirely, and some poorly‑written verifiers would accept the token as valid! Modern libraries fix this by letting the server specify which algorithm(s) it expects, rather than trusting whatever the token claims about itself.

5.3 · Verification steps · what happens when a server checks a JWT

When a request arrives with a JWT, the receiving server performs a precise sequence of checks:

1

Split the token

Split the string on the two dots to get header, payload, and signature segments.

2

Decode the header

Base64URL‑decode it to find the declared algorithm — but the server uses its own configured algorithm as the source of truth, not this field alone.

3

Recompute the signature

Using the server’s own secret or public key, recompute the signature over the received header.payload string.

4

Compare signatures

If they don’t match exactly (using a constant‑time comparison to avoid timing attacks), reject the token immediately.

5

Check registered claims

Verify exp hasn’t passed, nbf has passed, iss and aud match expectations.

6

Trust the payload

Only now does the application treat the claims inside as trustworthy and proceed with the request.

5.4 · A minimal Java example

Using the popular jjwt library, here’s what creating and verifying a JWT looks like in Java:

Java · jjwt · create and verify
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;

public class JwtDemo {

    // In real systems, load this secret from a secure vault, not code
    private static final SecretKey KEY = Keys.hmacShaKeyFor(
        "this-is-a-very-long-secret-key-value-32bytes".getBytes()
    );

    public static String createToken(String userId, String role) {
        long now = System.currentTimeMillis();
        return Jwts.builder()
                .setSubject(userId)
                .claim("role", role)
                .setIssuedAt(new Date(now))
                .setExpiration(new Date(now + 15 * 60 * 1000)) // 15 minutes
                .signWith(KEY)
                .compact();
    }

    public static Claims verifyToken(String token) {
        // Throws JwtException if signature invalid or token expired
        return Jwts.parserBuilder()
                .setSigningKey(KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public static void main(String[] args) {
        String token = createToken("user-42", "admin");
        System.out.println("Token: " + token);

        Claims claims = verifyToken(token);
        System.out.println("Subject: " + claims.getSubject());
        System.out.println("Role: " + claims.get("role"));
    }
}

Notice that createToken sets a short 15‑minute expiration, and verifyToken doesn’t need a database call — the jjwt library recomputes the signature in‑memory and throws an exception if it doesn’t match or if the token has expired.

5.5 · A worked Base64URL example

It’s worth seeing this transformation happen with real bytes, since it demystifies the “gibberish” at the start of every JWT. Take this tiny header:

Header JSON, exact bytes
{"alg":"HS256","typ":"JWT"}

As raw bytes, this JSON string is first Base64‑encoded, which would normally produce something like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. If the encoded output happened to contain a +, /, or trailing =, Base64URL swaps them: every + becomes -, every / becomes _, and padding = characters are stripped entirely. The result is guaranteed safe to drop straight into a URL query parameter, an HTTP header value, or a cookie, with zero additional escaping — which is precisely why the JWT designers chose it over plain Base64.

5.6 · Verifying a JWT inside a Spring Boot filter

In real Java web applications, JWT verification is usually wired into the request pipeline as a servlet filter, so every incoming request is checked once, automatically, before it ever reaches business logic:

Java · Spring Boot · JwtAuthFilter
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import io.jsonwebtoken.*;
import java.io.IOException;

public class JwtAuthFilter implements Filter {

    private final SecretKeyProvider keyProvider; // supplies current signing key(s)

    public JwtAuthFilter(SecretKeyProvider keyProvider) {
        this.keyProvider = keyProvider;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest) req;
        HttpServletResponse httpRes = (HttpServletResponse) res;

        String header = httpReq.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            httpRes.sendError(401, "Missing bearer token");
            return;
        }

        String token = header.substring(7);
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(keyProvider.currentKey())
                    .requireIssuer("https://auth.example.com")
                    .setAllowedClockSkewSeconds(30)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

            // Attach verified identity to the request for downstream handlers
            httpReq.setAttribute("userId", claims.getSubject());
            httpReq.setAttribute("role", claims.get("role"));

            chain.doFilter(req, res);
        } catch (ExpiredJwtException e) {
            httpRes.sendError(401, "Token expired");
        } catch (JwtException e) {
            httpRes.sendError(401, "Invalid token");
        }
    }
}

This pattern — verify once, at the boundary, then pass a trusted identity forward — is the same idea used by API gateways in cloud deployments, just implemented in application code instead of infrastructure.

06

Data Flow & Lifecycle

Let’s trace the full life of a JWT, from login to expiration, in a typical web application.

User Browser Auth Server API Service POST /login (user, pass) verify creds against DB generate JWT + sign 200 OK + access + refresh GET /orders  Authorization: Bearer JWT verify signature + exp locally · no DB call 200 OK + order data POST /refresh (refresh token) 200 OK + new access token
Figure 2 · The login and refresh sequence. Only the initial credential check needs a database. All subsequent API calls verify the JWT locally in each service.

Walking through this step by step, in plain language:

1

Login

The user submits their username and password to the authentication server.

2

Credential check

The auth server checks the password against its database — this is the only step in the whole flow that needs a database lookup.

3

Token issuance

The server builds a JWT containing the user’s ID and roles, signs it, and returns it to the browser along with a refresh token.

4

Storage

The browser or app stores the tokens (memory, secure cookie, or secure storage — more on this in the Security section).

5

Authenticated requests

Every subsequent API call attaches the JWT in the Authorization: Bearer <token> header.

6

Stateless verification

Each service verifies the signature and claims locally — no shared session store, no network hop.

7

Expiration

After the short‑lived access token expires, the client silently exchanges the refresh token for a new access token, without asking the user to log in again.

8

Logout / revocation

The client discards its tokens; the server may also blacklist the refresh token so it can never be reused.

07

Advantages, Disadvantages & Trade‑offs

Statelessness is not free. Here is a candid list of what JWT actually gains and what it costs in return.

✓ Advantages

  • Stateless — no shared session store required
  • Scales horizontally with zero extra coordination
  • Works naturally across domains and microservices
  • Self‑describing — carries roles / permissions inline
  • Standardized, wide library support in every language
  • Reduces database load for authentication checks

✗ Disadvantages

  • Cannot be easily “un‑issued” before expiry (revocation is hard)
  • Larger than a session ID — adds bytes to every request
  • Payload is readable by anyone — never put secrets in it
  • Clock skew between servers can cause false expirations
  • Stale claims — if a user’s role changes, old tokens still say the old role until they expire
  • Key management complexity (rotation, distribution)

7.1 · The revocation trade‑off, explained

This is the trade‑off that trips up almost everyone new to JWT. With a session, “logging a user out” is one line: delete the session row from the database. With a JWT, the token is valid and self‑verifying until its exp timestamp — the server has no built‑in way to say “actually, ignore this one,” because it never has to ask the server anything to be trusted.

The practical fix teams use is a hybrid approach: keep access tokens very short‑lived (so a compromised token is dangerous for only a few minutes), and manage revocation at the refresh token level, which usually IS stored server‑side and can be deleted or blacklisted instantly.

“JWT trades a small amount of ‘instant revocability’ for a large amount of scalability and simplicity — and most systems make that trade happily, as long as they keep access tokens short‑lived.”

7.2 · The “stale claims” problem, explained

Here’s a scenario that catches many teams off guard. Suppose an administrator demotes a user from "role": "admin" to "role": "member" at 2:00 PM. If that user already has a JWT issued at 1:55 PM with a 15‑minute expiration, that token is still cryptographically valid until 2:10 PM — and it still says "role": "admin", because the claims were baked in at issuance time and nothing about the database change is reflected inside a token that already exists.

This is the direct cost of statelessness: the server verifying the token doesn’t re‑check the database, so it can’t know the role changed. For most applications, a short access‑token lifetime (5–15 minutes) makes this an acceptable, bounded risk. For highly sensitive operations — changing a password, transferring money, revoking someone’s admin rights — many systems deliberately re‑verify against the database or require re‑authentication rather than trusting the token’s claims blindly, precisely because of this staleness window.

7.3 · When JWT is (and isn’t) the right choice

Good fit

Multi‑service APIs, mobile app backends, systems needing to scale horizontally without a shared session store, third‑party API authentication.

Poor fit

Simple single‑server apps where sessions are simpler; systems needing instant, precise permission changes; scenarios where token size on constrained networks (like SMS‑based systems) matters a lot.

08

Performance & Scalability

JWT’s biggest performance win is removing a network round‑trip. A session lookup against Redis over a network typically costs somewhere in the range of half a millisecond to a few milliseconds, plus connection overhead under load. Verifying a JWT signature happens entirely in local CPU and memory — often under a tenth of a millisecond for HMAC, and still very fast (though somewhat heavier) for RSA / ECDSA.

~0.05–0.2ms
Local HMAC verify
~0.3–1ms
Local RSA / ECDSA verify
1–5ms+
Networked session lookup
0
Extra DB queries per JWT check

At small scale this difference is invisible. At the scale of a service handling tens of thousands of requests per second, removing a mandatory network hop from the hot authentication path is a meaningful win for both latency and the load placed on the session store.

8.1 · The size trade‑off

A session cookie might be 20–40 bytes. A JWT with a handful of claims is commonly 200–1000+ bytes, and grows further with every claim you add. Multiply that by every request in a high‑traffic API and it becomes a real (if usually minor) bandwidth and header‑size cost — this is why experienced teams keep JWT payloads lean, storing only what’s needed to make authorization decisions, not entire user profiles.

8.2 · Asymmetric signing costs more CPU

RSA verification, while still fast, is more CPU‑intensive than HMAC verification. In systems verifying millions of tokens per second, teams often choose ES256 (ECDSA) over RS256 because elliptic‑curve operations are cheaper for equivalent security strength, or they cache verified results briefly to avoid re‑verifying an unchanged token on every single downstream call within a request chain.

09

High Availability & Reliability

Because JWT verification needs no external dependency, it actually improves system availability compared to session lookups: if the shared session store goes down, every service relying on it for auth goes down too. With JWT, as long as a service has the public key (or shared secret) loaded in memory, it can keep validating tokens even if the authentication server itself is temporarily unreachable.

Resilience benefit

This is a genuine architectural advantage: JWT decouples “can I verify who this user is” from “is the auth service currently up.” A payments service can keep accepting valid, unexpired tokens even during an auth‑service outage — though it obviously can’t issue new tokens or log anyone in during that window.

9.1 · Key rotation without downtime

Signing keys can’t live forever — they should be rotated periodically as a security hygiene practice. The standard pattern to avoid downtime is:

  1. Publish the new public key alongside the old one (services can verify with either).
  2. Start signing new tokens with the new private key.
  3. Wait until all tokens signed with the old key have naturally expired.
  4. Remove the old key from the trusted set.

This is exactly what the JWK Set (JWKS) endpoint is for — a well‑known URL (like /.well-known/jwks.json) that lists all currently valid public keys, each tagged with a kid (key ID) so verifiers know exactly which key signed a given token.

10

Security

JWT is a powerful tool, but it’s also one of the most commonly misused pieces of web security. Here are the failure modes every engineer should know.

10.1 · Storage location matters

StorageRisk
localStorageAccessible to any JavaScript on the page — vulnerable to XSS (cross‑site scripting) attacks stealing the token
Regular cookieSent automatically by the browser — vulnerable to CSRF (cross‑site request forgery) if not paired with protections
httpOnly, Secure, SameSite cookieNot readable by JavaScript (mitigates XSS theft) and can be locked down against CSRF — generally the safer default for browser apps
In‑memory (JS variable, not persisted)Safest against theft, but lost on page refresh — often combined with a refresh‑token cookie

10.2 · Common vulnerabilities

alg:none bypass

Trusting the algorithm claimed inside the token itself instead of the server’s own configuration.

Algorithm confusion

Tricking a server expecting RS256 into verifying with HS256, using the public key as if it were an HMAC secret.

Weak secrets

Short or guessable HMAC secrets that can be brute‑forced offline.

No expiration check

Forgetting to validate exp, letting stolen tokens live forever.

Sensitive data in payload

Storing passwords, SSNs, or tokens inside a JWT that’s technically readable by anyone.

No revocation strategy

No plan for what happens when a token needs to die early (compromised device, fired employee).

!
Golden rule

Always explicitly configure which algorithm(s) your verifier accepts on the server side, and never derive that choice from the incoming token itself.

10.3 · Java · verifying safely

Java · safe parser configuration
// Explicitly restrict accepted algorithms and validate all standard claims
Jwts.parserBuilder()
    .setSigningKey(KEY)
    .requireIssuer("https://auth.example.com")
    .setAllowedClockSkewSeconds(30) // tolerate small clock drift
    .build()
    .parseClaimsJws(token); // throws if alg mismatches, expired, or tampered

10.4 · Defense in depth

  • Use HTTPS everywhere — a JWT sent over plain HTTP can be intercepted and replayed.
  • Keep access tokens short‑lived (5–15 minutes is common).
  • Rotate signing keys periodically using a JWKS endpoint.
  • Store refresh tokens server‑side (or as httpOnly cookies) so they can be revoked.
  • Include a unique jti claim if you need per‑token revocation lists.
  • Validate aud (audience) so a token minted for one service can’t be replayed against another.

10.5 · CSRF and the cookie trade‑off

Putting a JWT in a plain cookie protects against XSS‑based theft (if marked httpOnly) but reopens the door to CSRF, because browsers attach cookies automatically to every request to that domain — including ones triggered by a malicious third‑party page the user happens to have open. The standard mitigation is to set the cookie’s SameSite attribute to Strict or Lax, which tells the browser not to send the cookie along with cross‑site requests, and to pair it with a separate CSRF token for state‑changing requests as defense in depth.

10.6 · Multi‑tenant audience validation

In systems serving multiple customers (tenants) from one identity provider, the aud claim becomes critical. Imagine “Auth Corp” issues tokens for both “Shopping App” and “Banking App.” Without checking aud, a token a user obtained for the low‑stakes Shopping App could potentially be replayed against the high‑stakes Banking API, since both trust the same issuer and signing key. Requiring each service to check that aud explicitly names it closes this gap.

10.7 · Rotating a signing key step by step

Here is what a real, zero‑downtime key rotation looks like in practice, tying back to the JWKS endpoint mentioned in the Reliability section:

1

Generate new key pair

Create a new private / public key pair and assign it a new kid, e.g. "2026-07-key".

2

Publish before use

Add the new public key to the JWKS endpoint before issuing any tokens with it, so verifiers already trust it.

3

Switch signing

The auth server starts signing new tokens with the new private key, tagging each token’s header with the new kid.

4

Dual‑trust window

For a period at least as long as the maximum token lifetime, verifiers keep trusting both the old and new public keys.

5

Retire the old key

Once every token signed with the old key has naturally expired, remove the old public key from the JWKS endpoint.

11

Monitoring, Logging & Metrics

Even though JWT removes a database dependency, production systems still need visibility into how tokens behave. Teams typically track:

Verification failure rate

A spike may indicate an expired signing key, a clock‑sync issue, or an active attack attempting forged tokens.

Token expiration events

High rates of expired‑token errors from legitimate users can mean access token lifetime is too short, or refresh logic is broken.

Refresh token usage

Unusual refresh patterns (many refreshes from new IP addresses) can signal token theft.

Key rotation events

Logged whenever a new kid is introduced or retired, for audit trails.

Structured logs should record the jti, sub, and kid for every authentication event (never the full token, and never sensitive claims) so security teams can trace an incident without re‑exposing credentials in log storage.

Tracing tip

In distributed tracing systems, propagate the token’s jti (or a hash of the token) as a correlation ID so you can follow one user’s request across every microservice it touches, without ever logging the raw token.

12

Deployment & Cloud Considerations

In cloud‑native systems, JWT is usually issued by a dedicated identity provider rather than hand‑rolled:

Managed IdPs

Services like Auth0, Okta, AWS Cognito, and Firebase Authentication issue and manage JWTs, handling key rotation and JWKS endpoints for you.

API gateways

Cloud gateways (AWS API Gateway, Kong, Envoy) can validate JWTs at the edge, rejecting invalid tokens before they ever reach application code.

Service mesh

In Kubernetes with a mesh like Istio, JWT validation can be enforced as a sidecar policy uniformly across every microservice.

A common cloud pattern: the API gateway verifies the JWT signature and basic claims once, then forwards a trusted, lightweight identity context (like headers with the user ID and roles) to internal services — so internal services don’t each need to re‑implement signature verification.

12.1 · Secrets management

Wherever signing keys live, they should never be hardcoded into source code or checked into version control. Cloud deployments typically pull the current signing key or key pair from a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, Google Secret Manager) at startup or on a rotation schedule, injected into the application as an environment variable or mounted file rather than a literal string in code. This also makes it straightforward to rotate a compromised key immediately, without a code deployment — only a secrets‑manager update and a restart (or hot‑reload, in systems built for it).

12.2 · Containerized and serverless environments

In containerized deployments (Docker, Kubernetes), each replica of a service is stateless by design — containers can be killed and recreated at any time. JWT fits this model naturally: a freshly started container needs no warm‑up call to a session store before it can start verifying requests, it only needs the public key or shared secret loaded into memory, which can be fetched once at boot from the JWKS endpoint or secrets manager. The same applies to serverless functions (AWS Lambda, Google Cloud Functions), where each invocation may run in a brand‑new execution environment — stateless JWT verification avoids adding a database round‑trip to every cold start.

Client JWT in header API Gateway verifies signature + exp once, at the edge Order Service trusts gateway headers Payment Service trusts gateway headers Notification Service trusts gateway headers Identity Provider issues JWTs · hosts JWKS trusted headers publishes public keys
Figure 3 · A typical cloud topology: the API gateway verifies each JWT once at the edge and forwards trusted headers to internal services, while a separate identity provider issues the tokens and publishes its public keys via a JWKS endpoint.
13

Storage, Caching & Revocation Strategies

Although JWTs are designed to avoid database lookups, real systems still combine them with storage layers for specific needs.

13.1 · Refresh token storage

Refresh tokens are typically stored in a database or fast key‑value store (like Redis) alongside the user ID, an expiration time, and a revoked flag. This lets a “log out everywhere” button work instantly, and lets administrators kill a compromised session on demand.

13.2 · Denylists (blacklists) for early revocation

Some systems maintain a short‑lived cache (Redis, with a TTL matching the token’s remaining lifetime) of jti values that have been explicitly revoked before their natural expiry — for example, if a user changes their password or an admin force‑logs‑out a compromised account. Every verification does a fast in‑memory cache lookup against this denylist in addition to the signature check.

Why a denylist and not an allowlist

An allowlist (storing every valid token) would defeat the purpose of JWT — you’d be back to a database lookup on every request. A denylist only needs to store the rare exceptions, and each entry can expire automatically once the token itself would have expired anyway, keeping it small.

13.3 · Caching verified claims

Within a single request that fans out across several internal calls, it’s common to verify the JWT once at the edge and cache the resulting decoded claims in the request context, rather than re‑verifying the same signature repeatedly as the request flows deeper into the system.

14

APIs & Microservices

JWT is especially well‑suited to microservice architectures because it decentralizes trust. Instead of every service calling a central “who is this user” API, each service independently verifies the token using a shared public key.

14.1 · Service‑to‑service tokens

Beyond user authentication, JWTs are also used for service‑to‑service authentication — one internal service proves its own identity to another using a JWT signed with its service credentials, often following the OAuth 2.0 “client credentials” flow.

14.2 · OAuth 2.0 and OpenID Connect

JWT is the backbone token format used inside two related but distinct standards:

OAuth 2.0

  • Focused on authorization — “what is this app allowed to do”
  • Issues access tokens (often JWTs) to call APIs on a user’s behalf

OpenID Connect (OIDC)

  • Built on top of OAuth 2.0, focused on authentication — “who is this user”
  • Adds a standardized id_token, which is always a JWT

This is why “Sign in with Google” buttons ultimately hand your application a JWT: OIDC standardized exactly what claims that identity token must contain (like sub, email, email_verified).

15

Design Patterns & Anti‑patterns

Every mature production JWT setup gravitates toward the same handful of good patterns, and steps on the same handful of well‑known landmines.

15.1 · Good patterns

  • Access + refresh token pair — short‑lived access tokens, long‑lived revocable refresh tokens.
  • Edge validation — verify once at the API gateway, pass trusted context downstream.
  • Key rotation via JWKS — never hardcode a single static key forever.
  • Minimal claims — only include what’s needed for authorization decisions.

15.2 · Anti‑patterns to avoid

Giant payload tokens

Stuffing an entire user profile into the JWT, bloating every request’s headers.

Never‑expiring tokens

Setting exp far in the future “for convenience,” eliminating the safety net short lifetimes provide.

Treating JWT as a session replacement everywhere

Using JWTs for things that genuinely need instant revocability and tight consistency, like admin permission changes.

Storing tokens insecurely

Putting long‑lived tokens in localStorage where any injected script can steal them.

!
Anti‑pattern spotlight

Using a single JWT with a long expiration as the only credential, with no refresh token and no revocation path, is one of the most common real‑world JWT mistakes — it optimizes for developer convenience at the cost of security.

15.3 · The “sidecar token translation” pattern

A more advanced pattern seen in large microservice fleets: rather than passing the raw JWT all the way through every internal hop, a sidecar proxy (running alongside each service instance) intercepts the incoming JWT, verifies it once, and translates it into a lightweight, internal‑only trust token — sometimes just a set of plain headers — that internal services trust implicitly because the sidecar is the only thing allowed to set them. This keeps the expensive cryptographic verification work concentrated at the network edge, while internal service‑to‑service calls stay fast and simple, relying on network‑level trust boundaries (like a service mesh’s mutual TLS) instead of re‑verifying a JWT signature at every hop.

This pattern trades a bit of architectural complexity (you now depend on the sidecar / mesh being correctly configured and genuinely un‑bypassable) for meaningfully lower per‑hop latency in deeply nested service call chains, which matters once a single user request might trigger a dozen or more internal calls.

16

Best Practices & Common Mistakes

A field‑guide checklist distilled from what mature teams actually do — and the surprisingly common things beginners get wrong.

Keep tokens short‑lived

5–15 minutes for access tokens is a common, sane default.

Always use HTTPS

Tokens sent over plain HTTP can be trivially intercepted.

Validate every registered claim

Don’t skip exp, aud, or iss checks even if it “seems to work” without them.

Pin the algorithm server‑side

Never trust the alg field from the token to choose how to verify it.

Rotate keys regularly

Treat signing keys like passwords — they age and should be replaced.

Never log full tokens

Log a hash or the jti, not the raw JWT string.

Common mistakes table

MistakeConsequenceFix
Storing JWT in localStorageXSS can steal itUse httpOnly cookies or in‑memory storage
No expiration setStolen token never diesAlways set a short exp
Trusting client‑declared algorithmalg:none / confusion attacksHardcode expected algorithm server‑side
Storing sensitive PII in payloadData exposure — payload is readableStore only IDs / roles; look up sensitive data server‑side
No refresh‑token revocation planCan’t force logout on compromiseStore refresh tokens server‑side with a revoked flag
17

Real‑World / Industry Examples

The same signed‑JSON idea quietly underpins “Sign in with Google,” countless mobile app backends, and the internal traffic of nearly every large microservice fleet. Here are six concrete deployments.

Google & OIDC

“Sign in with Google” issues an OpenID Connect id_token — a JWT — containing the user’s verified identity, consumed by millions of third‑party apps.

Auth0 / Okta

Identity‑as‑a‑service platforms that issue, sign, and manage JWTs on behalf of thousands of companies, handling key rotation via hosted JWKS endpoints.

AWS Cognito

Amazon’s managed identity service returns JWTs that API Gateway and Lambda can verify natively without custom auth code.

Netflix

Internally uses signed tokens across its large microservice fleet so that hundreds of independently deployed services can authenticate requests without a shared, centralized session dependency.

Uber

Uses token‑based authentication across its mobile and driver apps, allowing services distributed globally to validate a rider’s or driver’s identity locally, minimizing cross‑region latency.

Slack & Stripe APIs

Use bearer tokens (JWT‑based in various flows) to authenticate API calls from developer integrations without server‑side session state.

What these examples share is scale: each of them serves requests across many independently deployable services or partner integrations, which is exactly the environment where JWT’s stateless design pays off the most.

17.1 · A composite case study · e‑commerce checkout

To make this concrete, imagine a mid‑size e‑commerce company (a composite of common industry patterns, not any single real company) with services split roughly like this: a login service, a catalog service, a cart service, a payment service, and a shipping service, each independently deployed and separately scaled.

When a customer logs in, the login service issues a JWT containing their user ID and a tier claim (e.g., “standard” or “premium,” used to unlock free‑shipping perks). As the customer browses, adds items to their cart, and checks out, that same JWT is forwarded through an API gateway to each downstream service. The catalog service doesn’t need to know anything about how login works — it just verifies the token’s signature using a public key it fetched once from the login service’s JWKS endpoint, and trusts the sub and tier claims inside.

During a major sales event (think Black Friday), traffic to the cart and payment services can spike by 10x or more within minutes. Because none of these services depend on a shared session store to authenticate requests, each one can be scaled independently — adding more payment‑service instances doesn’t require also scaling a central session database, because there isn’t one in the authentication hot path. This is the concrete, practical payoff of the architectural trade‑offs discussed throughout this guide.

The one place statefulness does creep back in is the shopping cart itself — cart contents are genuinely mutable, shared state, so they’re stored in a database keyed by user ID, completely separate from the authentication layer. This illustrates an important principle: JWT solves the “who is this” problem statelessly, but it was never meant to solve “what has this user done” — that’s still the job of ordinary application data storage.

18

FAQ & Summary

Ten questions we hear repeatedly, followed by a distilled summary and the key takeaways from the entire guide.

Is a JWT encrypted?

No, not by default. A standard signed JWT (JWS) is readable by anyone who has the token — the signature only guarantees it hasn’t been tampered with. If you need to hide the contents, you’d use JWE (JSON Web Encryption) instead, or simply avoid putting sensitive data in the payload.

Can I revoke a JWT before it expires?

Not natively — that’s the core trade‑off of statelessness. Teams work around this with short expirations plus a server‑side denylist for the rare early‑revocation cases, or by managing revocation at the refresh‑token layer instead.

Where should I store a JWT on the client?

For browser apps, an httpOnly, Secure, SameSite cookie is generally the safer default because it’s inaccessible to JavaScript (mitigating XSS token theft). localStorage is easier to use but more exposed to script‑injection attacks.

What’s the difference between JWT and OAuth?

They solve different problems. OAuth 2.0 is an authorization framework — a set of flows for granting access. JWT is a token format that OAuth (and OpenID Connect) commonly use to represent the tokens they issue. You can use OAuth without JWTs (opaque tokens), and you can use JWTs without OAuth.

Why is my JWT rejected as “expired” right after I got it?

This is almost always a clock synchronization issue between the server that issued the token and the server verifying it. Keeping servers synced via NTP, and allowing a small clock‑skew tolerance (a few seconds) during verification, fixes this.

Should I use JWT for session management on a simple website?

Not necessarily. If you have one server (or a small cluster behind sticky sessions) and don’t need cross‑domain or microservice authentication, traditional server‑side sessions are simpler and give you instant revocation for free. JWT earns its complexity at scale.

What happens if two different services disagree on the current signing key?

Verification fails, and the request is rejected as unauthorized. This is why the JWKS‑based rotation process (publish new key → dual‑trust window → retire old key) exists: it guarantees every verifying service has a chance to learn about a new key before any token is signed with it, avoiding a window where valid tokens are wrongly rejected.

Can a JWT be used for something other than login authentication?

Yes. Beyond user login, JWTs are commonly used for one‑time email verification links, password‑reset links (with a very short expiration and a single‑use jti), service‑to‑service authentication, and even representing signed, verifiable data like event tickets or invitations — anywhere you need a compact, tamper‑evident claim that a receiving party can verify independently.

Is a longer secret key always better for HS256?

Up to a point, yes — HMAC‑SHA256 benefits from a secret with enough entropy (at minimum 256 bits / 32 random bytes is the common recommendation) so that offline brute‑force guessing isn’t feasible. Beyond that length, additional length gives rapidly diminishing security returns; what matters most is that the secret is genuinely random and never reused across environments (e.g., never sharing a production secret with staging).

Do I need both an access token and a refresh token?

For most production systems, yes. Using only a single long‑lived token forces an uncomfortable choice between poor security (long‑lived tokens that are dangerous if stolen) and poor usability (short‑lived tokens with no way to renew without forcing re‑login). The two‑token pattern gets you both short‑lived security and a smooth user experience.

Summary

A JWT is a compact, self‑contained, digitally signed token — three Base64URL parts joined by dots. It solves the scaling and cross‑service pain of centralized session stores by letting any server verify identity locally, without a database call, at the cost of instant revocability. The security of every JWT deployment is bounded by a small handful of decisions: pinning the algorithm on the server, checking every registered claim, choosing safe client storage, rotating keys through a JWKS endpoint, and pairing short‑lived access tokens with revocable refresh tokens.

A JWT is not magic — it is a small, tamper‑evident piece of JSON that trades “instant revocability” for “anyone can verify it, instantly, anywhere.” Most large systems find that trade well worth making, provided the security guardrails around it stay disciplined.

Key Takeaways

  • A JWT is a compact, self‑contained, digitally signed token — three Base64URL parts joined by dots: Header.Payload.Signature.
  • It’s signed, not encrypted by default — anyone can read the payload, but no one can alter it undetected without the key.
  • JWT solves the scaling and cross‑service pain of centralized session stores by letting any server verify identity locally, without a database call.
  • The main trade‑off is revocability — a valid JWT can’t be instantly killed, which is why short access‑token lifetimes plus revocable refresh tokens are the standard pattern.
  • Symmetric (HMAC) signing suits single‑backend systems; asymmetric (RSA / ECDSA) signing suits multi‑service systems where many verifiers shouldn’t be able to mint tokens.
  • Security hinges on correct implementation: pin the algorithm server‑side, always check expiration, use HTTPS, and never store sensitive secrets inside the payload.
  • JWT underpins modern identity standards like OAuth 2.0 and OpenID Connect, and is core infrastructure at companies like Google, Netflix, Uber, and Auth0.