JWT Tokens Explained: Structure, Validation, Pitfalls
JSON Web Tokens (JWTs) are everywhere in modern web development. If you have ever implemented authentication for a REST API, configured OAuth 2.0, or worked with single sign-on (SSO), you have encountered JWTs. They are the standard mechanism for stateless authentication in web applications and microservices. Yet many developers treat them as magic strings without understanding what is inside β and that leads to security mistakes. This guide breaks down the JWT structure (header, payload, signature), explains how to decode and validate tokens, covers the most common security pitfalls (including the none algorithm attack and localStorage vulnerabilities), and shows when JWTs are the right choice vs. server-side sessions. To quickly inspect any JWT, use our JWT Decoder β it runs entirely in your browser.
The Three Parts
Every JWT consists of three Base64URL-encoded sections separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
βββββββββ Header ββββββββββββββββββ Payload ββββββββββββββββββββββββ Signature βββββββββββββ
Header β metadata about the token:
{
"alg": "HS256",
"typ": "JWT"
}
Payload β the actual data (called βclaimsβ):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1716239022
}
Signature β cryptographic proof that the token hasnβt been tampered with. Created by signing the header + payload with a secret key:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Standard Claims
The JWT specification (RFC 7519) defines several registered claim names:
| Claim | Name | Purpose |
|---|---|---|
sub | Subject | Who the token is about (usually user ID) |
iss | Issuer | Who created the token |
aud | Audience | Who the token is intended for |
exp | Expiration | When the token expires (Unix timestamp) |
iat | Issued At | When the token was created |
nbf | Not Before | Token is not valid before this time |
jti | JWT ID | Unique identifier for the token |
You can also add any custom claims you need, like role, email, or permissions.
Encoding vs. Encryption
This is the most common misconception about JWTs: the payload is encoded, not encrypted. Anyone can decode a JWT and read its contents β no secret key needed. Base64URL is an encoding scheme, not a security mechanism.
This means you should never put sensitive data in a JWT payload:
- Passwords or password hashes
- Credit card numbers or financial data
- Private API keys or secrets
- Full social security numbers or government IDs
- Medical records or other regulated PII
The signature only guarantees integrity (the token hasnβt been modified), not confidentiality (the contents are hidden).
If you need encrypted tokens, look into JWE (JSON Web Encryption), which encrypts the payload so it cannot be read without the decryption key. However, JWE is significantly more complex and rarely needed β the better approach is simply not putting sensitive data in the token.
Decoding JWTs in Code
JavaScript / Node.js
// Decode without verification (for inspection only)
function decodeJwt(token) {
const [header, payload] = token.split('.').slice(0, 2)
.map(part => JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/'))));
return { header, payload };
}
// Verify with jsonwebtoken (Node.js)
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, secretOrPublicKey);
console.log(decoded.sub); // user ID
} catch (err) {
console.error('Invalid token:', err.message);
}
Python
import jwt # pip install PyJWT
# Decode without verification (inspection only)
payload = jwt.decode(token, options={"verify_signature": False})
# Decode with verification
try:
payload = jwt.decode(token, secret_key, algorithms=["HS256"])
print(payload["sub"])
except jwt.ExpiredSignatureError:
print("Token expired")
except jwt.InvalidTokenError:
print("Invalid token")
Go
// Using github.com/golang-jwt/jwt/v5
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(secretKey), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims["sub"])
}
Common Security Pitfalls
Not validating the signature. Decoding a JWT is easy. Verifying its signature requires the secret key or public key. Always verify server-side before trusting any claims.
Using the none algorithm. Some libraries accept "alg": "none" which means no signature at all. Always reject tokens with alg: none in production. Explicitly whitelist allowed algorithms:
// Good: explicitly specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });
// Bad: trusting whatever algorithm the token claims
jwt.decode(token); // never trust unverified tokens
Not checking expiration. Always validate the exp claim. An expired token should be rejected, period.
Storing JWTs in localStorage. This makes them vulnerable to XSS attacks. Any JavaScript running on your page (including injected scripts from third-party dependencies) can read localStorage. Prefer httpOnly cookies for browser-based applications.
Using overly long expiration times. JWTs cannot be revoked once issued (unless you maintain a blacklist). Short-lived tokens (15-60 minutes) with refresh tokens are the standard pattern.
Algorithm confusion attacks. An attacker might change the header to "alg": "HS256" and sign with the RS256 public key (which is often publicly available). Libraries that donβt enforce the expected algorithm are vulnerable. Always specify the expected algorithm when verifying.
HS256 vs RS256
The two most common signing algorithms:
HS256 (HMAC + SHA-256) β symmetric. The same secret key signs and verifies. Simple, but both parties need the secret.
Auth Server ββ(secret)ββ> signs token
API Server ββ(secret)ββ> verifies token
RS256 (RSA + SHA-256) β asymmetric. A private key signs, a public key verifies. Better for distributed systems where multiple services need to verify tokens but only one should create them.
Auth Server ββ(private key)ββ> signs token
API Server ββ(public key)βββ> verifies token (cannot forge tokens)
| HS256 | RS256 | |
|---|---|---|
| Key type | Shared secret | Public/private key pair |
| Key distribution | Secret must be shared (risky) | Only public key shared (safe) |
| Performance | Faster | Slower (RSA operations) |
| Best for | Single-server apps | Microservices, third-party verification |
When to Use JWTs
Good use cases:
- Stateless API authentication (the most common use)
- Single sign-on (SSO) across multiple services
- Short-lived authorization tokens in OAuth 2.0 flows
- Service-to-service communication in microservices
- One-time-use tokens (email verification, password reset)
Consider alternatives when:
- You need to revoke tokens instantly (use server-side sessions)
- Youβre building a simple single-server app (session cookies are simpler)
- You need to store large amounts of data (JWTs add overhead to every request)
- You need confidentiality (JWTs are transparent β consider JWE or opaque tokens)
The Access Token + Refresh Token Pattern
The standard approach for JWT-based authentication:
- User logs in β server issues an access token (short-lived, 15 min) and a refresh token (long-lived, 7 days)
- Client sends the access token with every API request
- When the access token expires, the client sends the refresh token to get a new access token
- If the refresh token is expired or revoked, the user must log in again
This balances security (short-lived access tokens limit damage from theft) with usability (users donβt have to log in every 15 minutes).
Try It Yourself
Use our JWT Decoder to paste any JWT and instantly see its decoded header, payload, and expiration status β all processed in your browser. Your tokens never leave your device.
For understanding the Base64 encoding used in JWTs, see our Base64 encoder and decoder, and learn how Base64 encoding works.
To understand the hash algorithms used in JWT signatures, check out our Hash Generator and the guide on MD5 vs SHA-256.
Further Reading
- RFC 7519 β JSON Web Token (JWT) specification
- RFC 7516 β JSON Web Encryption (JWE)
- RFC 6749 β OAuth 2.0 Authorization Framework
- jwt.io β JWT debugger and library directory
- OWASP JWT Cheat Sheet β Security best practices
- Auth0 JWT Introduction β Comprehensive JWT guide