Back to Blog
JWTAuthSecurity

JWT Explained: How JSON Web Tokens Work

April 14, 2026 · 8 min read

JSON Web Tokens (JWTs) are the most common way to represent authentication claims in modern web applications. They appear in Authorization headers, cookies, and query strings. Understanding their structure — and their weaknesses — is essential for any developer building authenticated APIs.

The Structure of a JWT

A JWT is three Base64URL-encoded JSON objects joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTg0MzIwMH0
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The three parts are: header, payload, and signature.

Header

Specifies the token type and the signing algorithm used:

{
  "alg": "HS256",   // HMAC-SHA256
  "typ": "JWT"
}

Payload (Claims)

Contains the actual data. Standard claims are defined by RFC 7519:

{
  "sub": "user_123",          // subject (user ID)
  "iss": "https://api.io9.me",// issuer
  "aud": "https://app.io9.me",// audience
  "iat": 1711756800,          // issued at (Unix timestamp)
  "exp": 1711843200,          // expiry (Unix timestamp)
  "role": "admin"             // custom claim
}

Important: the payload is Base64URL-encoded, not encrypted. Anyone can decode it. Never put passwords, secrets, or sensitive PII in a JWT payload.

Signature

The signature proves the token was issued by a trusted party and has not been tampered with:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

When your server receives a JWT, it recomputes this signature using the same secret (or public key for asymmetric algorithms). If the result matches, the token is valid. If anything in the header or payload was changed, the signature will not match.

Signing Algorithms

AlgorithmTypeUse when
HS256Symmetric (shared secret)Single service owns both issuing and verification
RS256Asymmetric (RSA)Different services verify tokens (private key signs, public key verifies)
ES256Asymmetric (ECDSA)Same as RS256 but with smaller keys and faster verification

For microservices, prefer RS256 or ES256: the auth service holds the private key and signs tokens, while downstream services only need the public key to verify. This way, a compromised downstream service cannot forge tokens.

Using JWTs in Practice

Issuing a token (Node.js)

import jwt from "jsonwebtoken";

const token = jwt.sign(
  { sub: "user_123", role: "admin" },
  process.env.JWT_SECRET,
  { expiresIn: "1h", issuer: "https://api.example.com" }
);

Verifying a token

try {
  const payload = jwt.verify(token, process.env.JWT_SECRET, {
    issuer: "https://api.example.com",
  });
  console.log(payload.sub); // "user_123"
} catch (err) {
  // TokenExpiredError, JsonWebTokenError, etc.
  return res.status(401).json({ error: "Invalid token" });
}

Sending a token

// Authorization header (most common for APIs)
Authorization: Bearer <token>

// HTTP-only cookie (more secure for web apps)
Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict

Common Security Mistakes

1. The "alg: none" attack

Early JWT libraries accepted tokens with "alg": "none" and no signature. Always explicitly specify which algorithms your server accepts during verification — never trust the algorithm declared in the header.

2. Weak secrets

For HS256, use a secret of at least 256 bits (32 random bytes). Short or guessable secrets can be brute-forced offline against a captured token. Generate them with openssl rand -base64 32.

3. Missing expiry

Always set exp. JWTs cannot be revoked server-side (unless you maintain a blocklist), so a short lifespan limits the damage from a stolen token. Access tokens should typically expire in 15 minutes to 1 hour; use refresh tokens for longer sessions.

4. Storing in localStorage

Storing JWTs in localStorage makes them accessible to any JavaScript on the page, including injected scripts (XSS). HTTP-only cookies are immune to XSS. Use cookies with HttpOnly; Secure; SameSite=Strict for web applications.

JWT vs Sessions

JWTs are stateless — the server does not need to look anything up. This makes them well-suited for distributed systems and microservices. Traditional sessions require a server-side store (Redis, database) but are easy to invalidate instantly. Neither is universally better; choose based on your architecture.

Use the Text Tools on io9.me to Base64-decode a JWT payload and inspect its claims directly in your browser.