JWT Tokens Explained: Header, Payload, Signature, and Security Checklist
Quick answer
What is a JWT token?
A JSON Web Token, defined by RFC 7519, is a compact URL-safe way to represent claims between two parties. A normal signed JWT has the shape header.payload.signature: the header identifies the token type and signing algorithm, the payload carries claims such as iss, sub, aud, and exp, and the signature proves the header and payload were not changed after signing. Decoding is not verification; RFC 8725-style verification must still check the allowed algorithm, key, signature, issuer, audience, expiration, and application authorization rules.
JWT shape
header.payload.signature
A compact signed JWT has three Base64url segments separated by dots.
Decode means read
not verified
Header and payload decoding only proves the token is parseable, not trusted.
Verify means trust check
alg + key + claims
Verification must check the allowed algorithm, signature key, time claims, issuer, and audience.
Payload is visible
not encrypted
Do not put passwords, API keys, session secrets, or private personal data in normal signed JWT claims.
What Is a JWT?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format used to securely transmit claims between two parties. JWTs are the backbone of modern authentication: when you log into an application and subsequent requests are authenticated without hitting the database, a JWT is almost certainly involved.
Unlike session-based authentication where the server normally stores session data, a signed JWT can be verified without a central session lookup. That does not mean every claim should be trusted blindly: the server still needs the expected key, allowed algorithm, issuer, audience, time checks, and authorization rules before accepting the token.
// A JWT looks like this (three Base64url-encoded parts separated by dots):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// Three parts:
// 1. Header → algorithm and token type
// 2. Payload → claims (user data, expiration, etc.)
// 3. Signature → cryptographic proof of authenticityTo decode and inspect any JWT instantly, paste it into our JWT Decoder. It splits the token into its three parts, decodes the header and payload, and shows the signature -- all in your browser without sending the token to any server.
The Three Parts of a JWT
1. Header
The header is a JSON object that specifies the signing algorithm and the token type. It is Base64Url-encoded to form the first part of the JWT.
{
"alg": "HS256", // signing algorithm
"typ": "JWT" // token type
}
// Base64Url-encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
// Common algorithms:
// HS256 — HMAC with SHA-256 (symmetric, shared secret)
// RS256 — RSA with SHA-256 (asymmetric, public/private key)
// ES256 — ECDSA with P-256 and SHA-256 (asymmetric, smaller keys)
// EdDSA — Edwards-curve DSA (asymmetric, modern, fast)2. Payload (Claims)
The payload contains claims -- statements about the user and metadata. Claims are categorized into three types: registered (standard), public (community-defined), and private (application-specific).
{
// Registered claims (standardized by RFC 7519)
"iss": "https://auth.example.com", // issuer
"sub": "user-12345", // subject (user ID)
"aud": "https://api.example.com", // audience
"exp": 1709856000, // expiration time (Unix timestamp)
"nbf": 1709852400, // not before (token not valid before this)
"iat": 1709852400, // issued at
"jti": "a1b2c3d4-e5f6-7890", // JWT ID (unique identifier)
// Private claims (application-specific)
"role": "admin",
"permissions": ["read", "write", "delete"],
"email": "[email protected]"
}| Claim | Full Name | Purpose |
|---|---|---|
| iss | Issuer | Who created and signed the token |
| sub | Subject | Who the token is about (usually user ID) |
| aud | Audience | Who the token is intended for |
| exp | Expiration | When the token expires (Unix timestamp) |
| nbf | Not Before | Token is invalid before this time |
| iat | Issued At | When the token was created |
| jti | JWT ID | Unique identifier (prevents replay attacks) |
3. Signature
The signature ensures the token has not been tampered with. It is created by encoding the header and payload, then signing them with the algorithm specified in the header.
// HS256 signature formula:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
// RS256 signature formula:
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
// The signature is what makes JWTs tamper-proof:
// If anyone changes the header or payload, the signature won't match
// If anyone tries to re-sign, they don't have the secret/private keyThe header and payload are Base64Url-encoded, not encrypted. Anyone can decode them -- the signature only proves authenticity, not confidentiality. To understand Base64 encoding in depth, read our Base64 Encoding Explained guide, or use our Base64 Encoder/Decoder tool.
Signing Algorithms: HS256 vs RS256 vs ES256
| Algorithm | Type | Key | Best For |
|---|---|---|---|
| HS256 | Symmetric | Shared secret (256 bits) | Monoliths, single-server apps |
| RS256 | Asymmetric | RSA key pair (2048+ bits) | Microservices, third-party verification |
| ES256 | Asymmetric | ECDSA P-256 key pair | Mobile apps, performance-sensitive |
| EdDSA | Asymmetric | Ed25519 key pair | Modern systems, fastest asymmetric |
// HS256: Same secret for signing AND verification
// ✓ Simple setup
// ✓ Fast
// ✗ Secret must be shared with every verifier
// ✗ If one service is compromised, all are
// RS256: Private key signs, public key verifies
// ✓ Only auth server has private key
// ✓ Any service can verify with public key
// ✓ Public key can be freely distributed (JWKS endpoint)
// ✗ Slower than HS256
// ✗ Larger key size
// ES256: Like RS256 but with smaller keys and faster signing
// ✓ 256-bit key ≈ 3072-bit RSA security
// ✓ Smaller tokens
// ✓ Faster than RS256
// ✗ Slightly less library support than RS256HS256 uses HMAC-SHA256 under the hood. To understand how HMAC works, see the HMAC section in our Hash Functions Explained guide. For generating secure secrets, use our Password Generator with maximum length and all character types enabled.
Creating and Verifying JWTs
Node.js with jsonwebtoken
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // at least 256 bits (32+ chars)
// Create a token
function createToken(user) {
return jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
},
SECRET,
{
expiresIn: '15m', // 15 minutes
issuer: 'https://api.example.com',
audience: 'https://app.example.com',
}
);
}
// Verify a token
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET, {
issuer: 'https://api.example.com',
audience: 'https://app.example.com',
algorithms: ['HS256'], // ALWAYS specify allowed algorithms
});
return { valid: true, payload: decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}
// Express middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
const result = verifyToken(token);
if (!result.valid) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = result.payload;
next();
}Python with PyJWT
# pip install pyjwt
import jwt
from datetime import datetime, timedelta, timezone
SECRET = "your-256-bit-secret"
# Create a token
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + timedelta(minutes=15),
"iss": "https://api.example.com",
}
return jwt.encode(payload, SECRET, algorithm="HS256")
# Verify a token
def verify_token(token: str) -> dict:
try:
return jwt.decode(
token,
SECRET,
algorithms=["HS256"], # ALWAYS specify algorithms
issuer="https://api.example.com",
)
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
# FastAPI dependency
from fastapi import Depends, HTTPException, Header
async def get_current_user(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid auth header")
token = authorization.split(" ")[1]
try:
return verify_token(token)
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))RS256 with Public/Private Keys
// Generate RSA key pair:
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
const fs = require('fs');
const jwt = require('jsonwebtoken');
const PRIVATE_KEY = fs.readFileSync('private.pem');
const PUBLIC_KEY = fs.readFileSync('public.pem');
// Auth server: sign with PRIVATE key
const token = jwt.sign({ sub: 'user-123' }, PRIVATE_KEY, {
algorithm: 'RS256',
expiresIn: '15m',
});
// Any service: verify with PUBLIC key (no secret needed)
const decoded = jwt.verify(token, PUBLIC_KEY, {
algorithms: ['RS256'],
});
// JWKS endpoint (JSON Web Key Set)
// Most auth providers expose their public keys at:
// https://auth.example.com/.well-known/jwks.json
// Services fetch the public key from this endpoint to verify tokensThe JWT Authentication Flow
// 1. User logs in
POST /auth/login
Body: { "email": "[email protected]", "password": "..." }
// 2. Server validates credentials, creates tokens
Response: {
"accessToken": "eyJhbGci...", // short-lived (15 min)
"refreshToken": "eyJhbGci...", // long-lived (7-30 days)
"expiresIn": 900 // seconds until access token expires
}
// refreshToken is also set as HttpOnly cookie
// 3. Client stores access token in memory (NOT localStorage)
let accessToken = response.accessToken;
// 4. Client sends access token with every API request
GET /api/users
Authorization: Bearer eyJhbGci...
// 5. When access token expires (401 response):
POST /auth/refresh
Cookie: refreshToken=eyJhbGci... // sent automatically
// 6. Server validates refresh token, issues new access token
Response: {
"accessToken": "eyJhbGci...", // new token
"expiresIn": 900
}
// 7. If refresh token is expired or invalid: redirect to loginSecurity Best Practices
- Always specify allowed algorithms -- The "none" algorithm attack exploits libraries that accept the algorithm from the token header. Always pass
algorithms: ['HS256'](or your chosen algorithm) when verifying. - Use short expiration for access tokens -- Access tokens should expire in 5-15 minutes. If a token is stolen, the window of vulnerability is limited.
- Store tokens securely -- Access tokens in memory (JavaScript variable). Refresh tokens in HttpOnly, Secure, SameSite=Strict cookies. Never in localStorage or sessionStorage.
- Validate all registered claims -- Check
exp(expiration),iss(issuer), andaud(audience) on every verification. Do not trust a token just because the signature is valid. - Implement token rotation for refresh tokens -- Issue a new refresh token every time one is used. If an old refresh token is replayed, invalidate all tokens for that user (indicating theft).
- Never put sensitive data in the payload -- JWTs are encoded, not encrypted. Anyone can decode the payload. Do not include passwords, SSNs, or API keys.
- Use strong secrets -- For HS256, use at least 256 bits (32 characters) of cryptographically random data. A weak secret can be brute-forced.
- Consider token size -- Every claim adds to the token size, and tokens are sent with every request. Keep payloads minimal. If you need extensive permissions data, fetch it from the server after verifying the token.
| Validation step | What to check before trusting a JWT |
|---|---|
| Token shape | Require three compact-serialization parts for a normal signed JWT. |
| Algorithm | Allow-list the expected alg value in server code; never let the token choose any algorithm. |
| Signature | Verify with the correct HMAC secret, public key, or JWKS key for that issuer and algorithm family. |
| Expiration | Reject expired exp values and honor nbf/not-before with a small clock-skew tolerance if needed. |
| Issuer | Match iss to the configured identity provider or auth server. |
| Audience | Match aud to the API or client that is supposed to accept the token. |
| Subject and type | Treat sub, typ, scope, and role claims as authorization inputs, not proof by themselves. |
| Key ID | Use kid only for key lookup after validation/sanitization; do not concatenate it into unsafe database or LDAP queries. |
Common JWT Attacks and Defenses
| Attack | How It Works | Defense |
|---|---|---|
| Algorithm: none | Attacker sets alg to "none", removes signature | Always specify allowed algorithms in verify() |
| Algorithm confusion | Switches RS256 to HS256, uses public key as HMAC secret | Specify algorithms; use separate verification paths for symmetric/asymmetric |
| Token theft (XSS) | JavaScript reads token from localStorage | Store tokens in memory or HttpOnly cookies, implement CSP headers |
| Brute-force secret | Weak HMAC secret is guessable | Use 256+ bit cryptographically random secrets |
| Replay attack | Stolen token is reused before expiration | Short expiration (5-15 min), jti claim for critical operations |
| Refresh token theft | Long-lived refresh token is stolen | Token rotation, reuse detection, HttpOnly cookies |
The "alg: none" Attack in Detail
// VULNERABLE code — DO NOT do this:
const decoded = jwt.verify(token, secret);
// This reads the algorithm from the token header!
// If the attacker sets "alg": "none", no verification happens
// SAFE code — always specify algorithms:
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'] // only accept HS256
});
// If the token has "alg": "none" or "alg": "RS256",
// verification will fail immediatelyToken Revocation Strategies
One of the biggest limitations of JWTs is that they cannot be revoked once issued (they are stateless). If a user logs out or their account is compromised, the access token remains valid until it expires. Here are strategies to address this.
| Strategy | How It Works | Trade-off |
|---|---|---|
| Short expiration | Access tokens expire in 5-15 minutes | Requires refresh flow; small revocation delay |
| Token blocklist | Store revoked token JTIs in Redis | Adds a database check (partially defeats stateless benefit) |
| Token version | Store version in user record; token has version claim | Requires DB lookup; changing version invalidates all tokens |
| Refresh token rotation | New refresh token per use; old one invalidated | Reuse detection catches theft; slightly more complex |
// Token blocklist with Redis (Node.js)
const Redis = require('ioredis');
const redis = new Redis();
// On logout: add token to blocklist
async function revokeToken(token) {
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`blocklist:${decoded.jti}`, ttl, '1');
}
}
// On every request: check blocklist
async function isTokenRevoked(token) {
const decoded = jwt.decode(token);
const revoked = await redis.get(`blocklist:${decoded.jti}`);
return !!revoked;
}
// Middleware
async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Missing token' });
if (await isTokenRevoked(token)) {
return res.status(401).json({ error: 'Token has been revoked' });
}
try {
req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}JWT vs Session Cookies: When to Use Each
| Factor | JWT | Session Cookie |
|---|---|---|
| State | Stateless (data in token) | Stateful (data on server) |
| Scalability | No shared state needed | Requires shared session store |
| Revocation | Difficult (needs blocklist) | Easy (delete from store) |
| Cross-domain | Works via Authorization header | Limited by cookie domain rules |
| Mobile apps | Native support | Requires cookie handling |
| Best for | APIs, microservices, mobile | Traditional web apps, SSR |
For a deeper comparison with code examples and architectural guidance, read our dedicated JWT vs Session Cookies article. For understanding the HTTP layer that carries these tokens, see our HTTP Status Codes Guide.
When NOT to Use JWTs
JWTs are not always the right choice. Here are situations where session-based authentication or other approaches work better.
- When immediate revocation is critical -- Banking, healthcare, and other high-security applications need the ability to instantly invalidate sessions. Session cookies do this naturally; JWTs require a blocklist.
- When you only have one server -- The stateless benefit of JWTs does not matter if you have a single server. Session cookies are simpler to implement and more secure by default.
- When token size matters -- JWTs can be 500+ bytes. If they are sent in cookies, you may hit the 4KB cookie size limit. Session IDs are typically 32-64 bytes.
- When you need to store lots of user data -- Putting extensive user data in the JWT payload makes it large. Store the data server-side and use the token only for identity.
Frequently Asked Questions
Where should I store JWT tokens on the client?
Store access tokens in memory (a JavaScript variable) and refresh tokens in HttpOnly, Secure, SameSite cookies. Never store tokens in localStorage or sessionStorage -- they are accessible to any JavaScript on the page, making them vulnerable to XSS attacks. HttpOnly cookies cannot be read by JavaScript, protecting against token theft. If you must use localStorage (for example, in mobile webviews), keep access tokens short-lived (5-15 minutes) and implement refresh token rotation.
What is the difference between HS256 and RS256?
HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification. It is simpler and faster, but every service that verifies tokens needs the same secret. RS256 (RSA-SHA256) uses a key pair: a private key for signing and a public key for verification. Only the auth server needs the private key; any service can verify tokens using the public key. Use HS256 for monolithic applications. Use RS256 for microservices where multiple services verify tokens independently.
How do I handle JWT token expiration and refresh?
Use a two-token strategy: a short-lived access token (5-15 minutes) and a long-lived refresh token (7-30 days). When the access token expires, send the refresh token to a dedicated endpoint to get a new access token without requiring re-login. Implement token rotation -- issue a new refresh token each time one is used, and invalidate the old one. If a refresh token is used twice (indicating theft), invalidate all tokens for that user.
Is decoding a JWT the same as verifying it?
No. Decoding only reads the Base64url-encoded header and payload. Verification checks the signature with the expected algorithm and key, then validates claims such as exp, nbf, iss, aud, token type, and authorization scope before the API trusts it.
Which JWT claims should an API validate?
Validate at least the time claims, issuer, audience, token type, and application-specific authorization claims after the signature passes. RFC 8725 also calls out explicit algorithm verification and warns that received claims should not be trusted just because they decode.
Source Checkpoint: JWT Standards Used Here
This guide was refreshed against primary IETF sources on May 30, 2026. The practical advice above separates the token format standard from the security best-practice update and the underlying JOSE signing/algorithm specifications.
| Source | Use in this guide | Link |
|---|---|---|
| RFC 7519 | Defines JSON Web Token structure, registered claims, compact serialization, and JWT terminology. | Open IETF source |
| RFC 8725 | Best Current Practice that updates RFC 7519 with secure deployment guidance such as algorithm verification and issuer/audience validation. | Open IETF source |
| RFC 7515 | Defines JSON Web Signature, the signing layer used by signed JWTs. | Open IETF source |
| RFC 7518 | Defines JSON Web Algorithms identifiers for JOSE/JWS/JWE/JWK implementations. | Open IETF source |
Decode and Inspect JWT Tokens
Paste any JWT into our free decoder to see the header, payload, and signature. Verify the structure, check claims, and debug authentication issues -- all in your browser.
Open JWT DecoderRelated Articles
JWT Tokens Deep Dive
Extended reference covering JWKS endpoints, token introspection, and OAuth2 flows.
JWT vs Session Cookies
Compare stateful and stateless authentication methods for web and mobile.
Hash Functions Explained
MD5, SHA-256, bcrypt -- how the HMAC behind HS256 actually works.
Base64 Encoding Explained
How JWT header and payload are encoded and why Base64Url differs from Base64.