JWT Tokens Explained: Authentication Made Simple
That long string of gibberish in your Authorization header? That's a JWT—and understanding how it works is essential for modern web development. This guide breaks down JWT structure, validation, and the security pitfalls that trip up even experienced developers.
What's Actually Inside a JWT?
A JSON Web Token looks like random characters, but it's actually three Base64-encoded JSON objects separated by dots. Here's what each part contains:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cHeader
Contains the token type (JWT) and signing algorithm (HS256, RS256). This tells the server how to verify the signature.
Payload
Contains "claims"—data like user ID, email, roles, and expiration time. Anyone can decode and read this, so never include secrets!
Signature
Created by hashing the header + payload with a secret key. This proves the token wasn't tampered with.
Critical Understanding
JWTs are signed, not encrypted. Anyone can decode and read the payload—they just can't modify it without invalidating the signature. Never put sensitive data like passwords or API keys in JWT payloads.
How JWT Authentication Actually Works
Understanding the flow helps you implement JWT correctly and debug issues:
User Logs In
User sends credentials to your auth endpoint. Server verifies against database.
Server Creates JWT
Server creates a JWT with user claims (ID, roles) and signs it with a secret key.
Client Stores Token
Client stores the JWT (in memory, localStorage, or HttpOnly cookie).
Client Sends Token with Requests
Every API request includes the JWT in the Authorization header: Bearer eyJhbG...
Server Validates Token
Server verifies signature, checks expiration, and extracts user info from payload. No database lookup needed!
Practical Code Examples
Decoding a JWT Without Verification (Client-Side)
// Decode JWT payload without verification (for client-side use only!)
function decodeJwtPayload(token) {
try {
const base64Payload = token.split('.')[1];
const payload = atob(base64Payload);
return JSON.parse(payload);
} catch (error) {
console.error('Invalid JWT format');
return null;
}
}
// Usage
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const payload = decodeJwtPayload(token);
console.log(payload);
// { sub: "1234567890", name: "John", iat: 1516239022 }
// Check if token is expired
function isTokenExpired(token) {
const payload = decodeJwtPayload(token);
if (!payload || !payload.exp) return true;
return Date.now() >= payload.exp * 1000;
}Attaching JWT to API Requests
// Axios interceptor for automatic JWT attachment
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.example.com' });
api.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle token expiration
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token expired - try refresh or redirect to login
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
try {
const { data } = await axios.post('/auth/refresh', { refreshToken });
localStorage.setItem('accessToken', data.accessToken);
error.config.headers.Authorization = `Bearer ${data.accessToken}`;
return api.request(error.config);
} catch {
localStorage.clear();
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);Common JWT Security Mistakes
Not Validating the Signature
Never trust a JWT without verifying its signature server-side. Attackers can modify the payload and create their own "valid" tokens if you only decode without verification.
Using "none" Algorithm
Some JWT libraries accept alg: "none" which means no signature required. Always explicitly specify and validate the expected algorithm. Reject tokens with unexpected algorithms.
Storing Sensitive Data in Payload
JWTs are encoded, not encrypted. Anyone with the token can read the payload. Store user ID and roles, but never passwords, SSNs, or other sensitive information.
No Token Expiration
Always set reasonable expiration times (exp claim). Long-lived tokens that get stolen give attackers extended access. Use short access tokens with refresh tokens for better security.
Frequently Asked Questions
Should I store JWTs in localStorage or cookies?
HttpOnly cookies are generally safer because JavaScript can't access them, protecting against XSS attacks. localStorage is more convenient but vulnerable to XSS. For high-security applications, use HttpOnly cookies with SameSite=Strict and CSRF tokens. For SPAs with trusted content, localStorage with short expiration times can work.
What's the difference between JWT signing algorithms HS256 and RS256?
HS256 uses a shared secret key for both signing and verification—simpler but requires sharing the secret with every service. RS256 uses asymmetric keys: a private key for signing and a public key for verification. RS256 is better for distributed systems where you don't want to share secrets across services.
How long should JWT access tokens last?
Access tokens should be short-lived: 15-60 minutes for most applications. Use refresh tokens (stored securely) to obtain new access tokens without requiring users to re-authenticate. This limits damage if a token is stolen while maintaining good user experience.
Wrapping Up
JWTs are powerful for stateless authentication, but they require careful implementation. Always verify signatures server-side, use short expiration times, and never store sensitive data in payloads. Remember: JWTs are signed (tamper-proof) but not encrypted (not secret).
Next steps: Decode your application's JWT using our tool to understand its structure, then review your token storage strategy based on the security best practices above.
Related Resources
Internal Links:
External References: