JWT Tokens Explained: Structure, Security & Common Mistakes
JSON Web Tokens (JWTs) are everywhere in modern web development — powering authentication, authorization, and API security. But misunderstanding how they work leads to security vulnerabilities that get applications breached. This guide covers JWT structure, signing algorithms, security best practices, and the mistakes you need to avoid.
🔑 Try the Tool
Decode any JWT instantly — view header, payload, expiration status, and claim explanations.
Open JWT Decoder →What Is a JWT?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format defined by RFC 7519. It provides a standardized way to transmit claims — assertions about an entity (typically a user) — between two parties.
JWTs solve a fundamental problem in stateless architectures: how does a server know who you are without checking a database on every request? After authentication, the server issues a JWT containing the user's identity and permissions. The client sends this token with each subsequent request, and the server can verify it using cryptographic signatures — no database lookup needed.
JWTs are used for:
- Authentication: After login, the server issues a JWT. Each request includes this token to prove identity.
- Authorization: The token's claims (roles, permissions, scopes) determine what the user can access.
- Information exchange: JWTs can carry any JSON data between parties with integrity guarantees.
- Single Sign-On (SSO): One token works across multiple services in the same ecosystem.
JWT Structure: Three Parts, Three Purposes
Every JWT consists of three Base64URL-encoded parts separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxaOoaBSTPoqkm
Breaking this down:
1. Header
The header is a JSON object that identifies the token type and the signing algorithm:
{
"alg": "RS256",
"typ": "JWT"
}
Common header fields:
alg— The signing algorithm (HS256, RS256, ES256, etc.)typ— The token type, always "JWT"kid— Key ID, identifying which key was used to sign (useful for key rotation)
2. Payload
The payload contains claims — key-value pairs carrying information. RFC 7519 defines seven registered claims:
| Claim | Name | Description |
|---|---|---|
iss | Issuer | Who created the token (e.g., your auth server URL) |
sub | Subject | Who the token is about (usually a user ID) |
aud | Audience | Who the token is intended for |
exp | Expiration | Unix timestamp when the token expires |
nbf | Not Before | Unix timestamp before which the token is not valid |
iat | Issued At | Unix timestamp when the token was created |
jti | JWT ID | Unique identifier for the token (prevents replay) |
You can also add any custom claims you need — roles, permissions, email, name, etc. But remember: the payload is readable by anyone. It's Base64URL-encoded, not encrypted.
3. Signature
The signature ensures the token hasn't been tampered with. It's created by signing the encoded header and payload with a secret key:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The signature is the only part that provides security. Without it (or with "alg": "none"), anyone can modify the payload and the server won't know.
Base64URL: Not Base64
JWTs use Base64URL encoding, not standard Base64. The difference matters:
+is replaced with-/is replaced with_- Trailing
=padding is removed
This makes JWTs safe to use in URLs, HTTP headers, and cookies without percent-encoding. If you try to decode a JWT with standard atob(), you'll need to convert it first:
// Decode JWT payload in JavaScript
function decodeJwtPayload(token) {
const payload = token.split('.')[1];
// Convert Base64URL to standard Base64
const base64 = payload
.replace(/-/g, '+')
.replace(/_/g, '/');
// Add padding
const padded = base64 + '=='.slice(0, (4 - base64.length % 4) % 4);
return JSON.parse(atob(padded));
}
// Python
import base64, json
payload = token.split('.')[1]
# Add padding and decode
decoded = base64.urlsafe_b64decode(payload + '=='*(-len(payload) % 4))
claims = json.loads(decoded)
Signing Algorithms: HS256 vs RS256
The two most common JWT signing algorithms have fundamentally different security models:
| Property | HS256 (Symmetric) | RS256 (Asymmetric) |
|---|---|---|
| Key type | Shared secret | Private key + public key pair |
| Who signs? | Anyone with the secret | Only the private key holder |
| Who verifies? | Anyone with the secret | Anyone with the public key |
| Best for | Single server, internal APIs | Microservices, third-party consumers |
| Key distribution | Secret must be shared carefully | Public key can be shared openly |
| Performance | Faster (no asymmetric crypto) | Slower but more secure for distributed systems |
If the same service issues and verifies tokens, HS256 is fine. If different services need to verify tokens (microservices, mobile apps, third-party integrations), use RS256 so you never share the signing key.
JWT Security Best Practices
1. Always Validate the Signature
Never trust a JWT without verifying its signature. Decoding is not verification. Libraries like jsonwebtoken (Node.js) handle this automatically — don't roll your own.
2. Set Short Expiration Times
Access tokens should expire quickly — 5 to 15 minutes for sensitive applications. Use refresh tokens (stored server-side or in HTTP-only cookies) to issue new access tokens without re-authentication.
3. Validate All Claims
Don't just check the signature. Also validate:
exp— Is the token expired?nbf— Is the token being used too early?iss— Did the expected issuer create this token?aud— Is this token intended for your service?
4. Don't Put Secrets in the Payload
The payload is not encrypted — anyone can decode it with a single line of code. Never include passwords, API keys, credit card numbers, social security numbers, or any sensitive data in a JWT payload.
5. Use HTTPS Everywhere
JWTs sent over HTTP can be intercepted. Always transmit tokens over HTTPS. Store them in HTTP-only, Secure, SameSite cookies when possible (not localStorage, which is vulnerable to XSS).
6. Handle Key Rotation
Use the kid (Key ID) header claim and maintain a JWKS (JSON Web Key Set) endpoint. This lets you rotate signing keys without invalidating all existing tokens.
Common JWT Mistakes
The none algorithm means the token has no signature. If your server accepts alg: none tokens, an attacker can forge any token. Always reject unsigned tokens in production. Most JWT libraries default to rejecting none, but verify your configuration.
Mistake #2: Confusing encoding with encryption. JWT payloads are Base64URL-encoded, not encrypted. Encoding is a format, not a security mechanism. If you need the payload to be unreadable, use JWE (JSON Web Encryption) or keep sensitive data out of the token entirely.
Mistake #3: Storing JWTs in localStorage. localStorage is accessible to any JavaScript running on your page. If you have an XSS vulnerability, an attacker can steal the token. HTTP-only cookies are safer because JavaScript can't access them.
Mistake #4: Not checking expiration. If your server doesn't check the exp claim, stolen tokens work forever. Always validate expiration on every request.
Mistake #5: Bloating the payload. Every JWT is sent with every request. A token with 50 custom claims wastes bandwidth and hits header size limits. Keep payloads minimal — store detailed user data server-side and reference it by ID.
Mistake #6: No token revocation strategy. JWTs are stateless — there's no "logout" mechanism built in. If a token is compromised, it's valid until it expires. Solutions: short expiration times, token blacklists (backed by Redis or a database), or refresh token rotation.
JWT vs Sessions: When to Use What
JWTs and server-side sessions solve the same problem differently:
| Factor | JWT | Server-side Session |
|---|---|---|
| State | Stateless (token contains all data) | Stateful (server stores session data) |
| Scalability | No shared state — scales horizontally | Needs shared session store (Redis, DB) |
| Revocation | Hard — token valid until expiry | Easy — delete the session record |
| Size | Can be large (all claims in token) | Small (just a session ID cookie) |
| Best for | APIs, microservices, SPAs | Traditional web apps, when revocation matters |
Neither is universally better. Use JWTs when you need stateless authentication across multiple services. Use sessions when you need instant revocation and don't mind the shared state.
Decoding JWTs in Code
JavaScript (Browser)
// Quick decode without a library
const [header, payload] = token.split('.').slice(0, 2).map(part => {
const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(base64));
});
console.log('Algorithm:', header.alg);
console.log('Expires:', new Date(payload.exp * 1000));
console.log('User:', payload.sub);
Node.js (with verification)
const jwt = require('jsonwebtoken');
// Verify and decode (ALWAYS verify in production)
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
});
console.log('Valid token for user:', decoded.sub);
} catch (err) {
console.error('Token rejected:', err.message);
}
Python
import jwt # pip install PyJWT
# Verify and decode
try:
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://api.example.com",
issuer="https://auth.example.com"
)
print(f"Valid token for user: {decoded['sub']}")
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidTokenError as e:
print(f"Token rejected: {e}")
Key Takeaways
- JWTs are three Base64URL parts: header (algorithm), payload (claims), signature (integrity).
- Decoding is not verification. Always verify signatures server-side before trusting claims.
- The payload is not encrypted. Anyone can read it. Keep sensitive data out.
- Use short-lived tokens with refresh tokens for the best security/UX balance.
- Choose HS256 for single-server, RS256 for distributed systems.
- Validate all claims —
exp,iss,aud— not just the signature. - Have a revocation strategy — short expiration, blacklists, or refresh token rotation.
🔑 Decode JWTs Instantly
Paste a JWT to view header, payload, signature, expiration status, and claim explanations — all client-side.
Open JWT Decoder →