JWT Token Structure — Header, Payload, and Signature Explained
A JWT has three Base64URL-encoded parts: header, payload, and signature, separated by dots. Here's how each part works, what claims mean, and how to read and decode a JWT token.
A JWT (JSON Web Token) is three Base64URL-encoded JSON objects joined by dots: header.payload.signature. The header and payload are readable by anyone; the signature verifies authenticity.
Use the JWT Decoder to decode and inspect any JWT token.
JWT structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Split by .:
Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Part 1: Header
Decoded:
{
"alg": "HS256",
"typ": "JWT"
}
alg— signing algorithm:HS256,RS256,ES256, etc.typ— type identifier, always"JWT"
Less common header fields:
kid— key ID (which signing key to use)x5t— X.509 certificate thumbprintjku— JWK Set URL
Part 2: Payload (Claims)
Decoded:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Standard registered claims (RFC 7519)
| Claim | Full Name | Description |
|---|---|---|
iss | Issuer | Who issued the token ("auth.example.com") |
sub | Subject | Who the token is about (user ID) |
aud | Audience | Who should accept the token ("api.example.com") |
exp | Expiration | Unix timestamp when token expires |
nbf | Not Before | Unix timestamp before which token is invalid |
iat | Issued At | Unix timestamp when token was issued |
jti | JWT ID | Unique identifier for the token |
Full example payload
{
"iss": "https://auth.example.com",
"sub": "user_abc123",
"aud": "https://api.example.com",
"exp": 1748649600,
"iat": 1748563200,
"jti": "token_xyz789",
"email": "alice@example.com",
"roles": ["admin", "editor"],
"plan": "pro"
}
Custom claims (email, roles, plan) are application-specific data.
Part 3: Signature
The signature verifies the token wasn’t tampered with:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
For HS256, the server signs with a secret key and verifies with the same key. For RS256, the server signs with a private key and anyone can verify with the public key.
Critical: The signature does NOT encrypt the payload. The payload is Base64URL-encoded (reversible), not encrypted. Anyone can decode and read a JWT. Never put sensitive data in the payload unless you encrypt the token (JWE).
Decoding in code
JavaScript
// Decode without verification (read claims):
function decodeJwt(token) {
const [header, payload, signature] = token.split('.');
const decode = (part) => {
const padded = part.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(padded));
};
return {
header: decode(header),
payload: decode(payload),
signature,
};
}
const { payload } = decodeJwt(token);
console.log(payload.sub, payload.exp);
// Verify and decode with jsonwebtoken:
import jwt from 'jsonwebtoken';
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub); // user ID
} catch (err) {
if (err.name === 'TokenExpiredError') console.log('Token expired');
if (err.name === 'JsonWebTokenError') console.log('Invalid token');
}
Python
import base64
import json
def decode_jwt(token: str) -> dict:
header_b64, payload_b64, signature = token.split('.')
def decode_part(part):
# Add padding if needed:
padded = part + '=' * (4 - len(part) % 4)
return json.loads(base64.urlsafe_b64decode(padded))
return {
'header': decode_part(header_b64),
'payload': decode_part(payload_b64),
'signature': signature,
}
# With PyJWT (includes verification):
import jwt
try:
payload = jwt.decode(token, secret, algorithms=['HS256'])
print(payload['sub'])
except jwt.ExpiredSignatureError:
print('Token expired')
except jwt.InvalidTokenError:
print('Invalid token')
Checking expiration manually
function isTokenExpired(token) {
const { payload } = decodeJwt(token);
if (!payload.exp) return false;
return Date.now() / 1000 > payload.exp;
}
function getExpiresIn(token) {
const { payload } = decodeJwt(token);
if (!payload.exp) return null;
const secondsLeft = payload.exp - Date.now() / 1000;
return Math.max(0, Math.floor(secondsLeft));
}
What Base64URL is
JWT uses Base64URL (not standard Base64) — it replaces + with - and / with _, and omits padding =. This makes tokens safe for use in URLs without encoding.
// Standard Base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
// Base64URL: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
Related tools
- JWT Decoder — decode and inspect JWT tokens online
- JWT Best Practices — security recommendations
- JWT Authentication Guide — implementing JWT auth
Related posts
- Decoding a JWT Is Not the Same as Verifying It — Every JWT bug in production reduces to the same mistake: trusting a decoded toke…
- JWT Security Checklist for 2026 — Twelve checks every JWT implementation should pass before shipping. The actual c…
- JWT Authentication Flow — Login, Token Storage, Refresh Tokens — JWT authentication involves issuing tokens on login, sending them with requests,…
- JWT Best Practices — Secure JSON Web Token Implementation — JWT implementation mistakes lead to authentication bypasses and security vulnera…
Related tool
Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.
Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.