JSON Web Tokens (JWT) have become the industry standard for API authentication, powering millions of applications worldwide. This comprehensive guide will teach you how to implement secure, scalable JWT authentication from scratch, with practical examples and security best practices.
What is JWT and Why Use It?
A JSON Web Token is a compact, URL-safe token format for securely transmitting information between parties. Unlike session-based authentication, JWTs are stateless—the server doesn’t need to store session data, making them ideal for distributed systems and microservices.
JWT Structure
A JWT consists of three Base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsImV4cCI6MTY4MzgwMDAwMH0.signature_hash
Breaking it down:
- Header: Algorithm and token type
- Payload: Claims (user data, permissions, expiration)
- Signature: Cryptographic signature ensuring integrity
Decoded example:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"userId": "12345",
"email": "[email protected]",
"role": "admin",
"iat": 1683700000,
"exp": 1683800000
}
Important: JWTs are signed, not encrypted. Never store sensitive data like passwords in the payload[1].
Implementation: Node.js + Express
Let’s build a complete authentication system with registration, login, and protected routes.
Step 1: Setup and Dependencies
npm init -y
npm install express jsonwebtoken bcrypt dotenv express-validator
npm install --save-dev nodemon
Create your environment variables:
# .env
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
PORT=3000
Step 2: Create Authentication Middleware
// middleware/auth.js
const jwt = require('jsonwebtoken');
const authenticateToken = (req, res, next) => {
// Get token from header
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: 'Access denied. No token provided.'
});
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired.'
});
}
return res.status(403).json({
error: 'Invalid token.'
});
}
};
module.exports = { authenticateToken };
Step 3: User Registration
// routes/auth.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const router = express.Router();
// In-memory user store (use a database in production)
const users = [];
// Registration endpoint
router.post('/register', [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 })
.withMessage('Password must be at least 8 characters'),
body('name').trim().notEmpty()
], async (req, res) => {
// Validate input
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password, name } = req.body;
// Check if user exists
if (users.find(u => u.email === email)) {
return res.status(400).json({
error: 'User already exists'
});
}
try {
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create user
const user = {
id: users.length + 1,
email,
password: hashedPassword,
name,
role: 'user',
createdAt: new Date()
};
users.push(user);
// Generate token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
res.status(201).json({
message: 'User created successfully',
token,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
res.status(500).json({
error: 'Error creating user'
});
}
});
module.exports = router;
Step 4: User Login
// routes/auth.js (continued)
router.post('/login', [
body('email').isEmail().normalizeEmail(),
body('password').notEmpty()
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
// Find user
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({
error: 'Invalid credentials'
});
}
try {
// Verify password
const isValidPassword = await bcrypt.compare(
password,
user.password
);
if (!isValidPassword) {
return res.status(401).json({
error: 'Invalid credentials'
});
}
// Generate access token
const accessToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
// Generate refresh token (for token refresh flow)
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
res.json({
message: 'Login successful',
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
res.status(500).json({
error: 'Error during login'
});
}
});
Step 5: Protected Routes
// routes/users.js
const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const router = express.Router();
// Protected route - requires valid JWT
router.get('/profile', authenticateToken, (req, res) => {
// req.user is populated by authenticateToken middleware
res.json({
message: 'Access granted to protected route',
user: req.user
});
});
// Admin-only route
const requireAdmin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({
error: 'Admin access required'
});
}
next();
};
router.get('/admin', authenticateToken, requireAdmin, (req, res) => {
res.json({
message: 'Admin access granted',
data: 'Sensitive admin data here'
});
});
module.exports = router;
Step 6: Complete Server Setup
// server.js
require('dotenv').config();
const express = require('express');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const app = express();
// Middleware
app.use(express.json());
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Testing Your Authentication
Register a New User
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "securepassword123",
"name": "John Doe"
}'
Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "securepassword123"
}'
Access Protected Route
curl http://localhost:3000/api/users/profile \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
Implementing Token Refresh
Refresh tokens allow users to get new access tokens without re-authenticating:
// routes/auth.js (add this route)
router.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
error: 'Refresh token required'
});
}
try {
// Verify refresh token
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET
);
// Generate new access token
const accessToken = jwt.sign(
{
userId: decoded.userId,
email: decoded.email,
role: decoded.role
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
res.json({ accessToken });
} catch (error) {
res.status(403).json({
error: 'Invalid refresh token'
});
}
});
Security Best Practices
1. Use Strong Secrets
// Generate a secure secret
const crypto = require('crypto');
const secret = crypto.randomBytes(64).toString('hex');
console.log(secret); // Use this in production
2. Set Appropriate Expiration Times
| Token Type | Recommended Duration | Use Case |
|---|---|---|
| Access Token | 15-60 minutes | API calls |
| Refresh Token | 7-30 days | Token renewal |
| Remember Me | 30-90 days | Optional UX |
3. Implement Token Blacklisting
For logout functionality, maintain a blacklist of invalidated tokens:
// Simple in-memory blacklist (use Redis in production)
const tokenBlacklist = new Set();
router.post('/logout', authenticateToken, (req, res) => {
const token = req.headers['authorization'].split(' ')[1];
// Add token to blacklist
tokenBlacklist.add(token);
res.json({ message: 'Logged out successfully' });
});
// Update authenticateToken middleware
const authenticateToken = (req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// Check blacklist
if (tokenBlacklist.has(token)) {
return res.status(401).json({ error: 'Token has been revoked' });
}
// ... rest of verification
};
4. HTTPS Only
Always use HTTPS in production to prevent token interception:
// Force HTTPS
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.redirect('https://' + req.headers.host + req.url);
}
next();
});
5. Rate Limiting
Prevent brute-force attacks:
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests per window
message: 'Too many login attempts, please try again later.'
});
router.post('/login', authLimiter, async (req, res) => {
// ... login logic
});
JWT vs Session Comparison
| Feature | JWT | Sessions |
|---|---|---|
| Storage | Client-side | Server-side |
| Scalability | Excellent (stateless) | Requires session store |
| Revocation | Complex (needs blacklist) | Easy (delete session) |
| Size | Larger (sent with each request) | Smaller (just session ID) |
| Security | Vulnerable if secret leaked | Vulnerable if session hijacked |
| Best For | Microservices, APIs, SPAs | Traditional web apps |
Advanced: JWT with TypeScript
// types/auth.ts
export interface JWTPayload {
userId: number;
email: string;
role: 'admin' | 'user';
iat?: number;
exp?: number;
}
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { JWTPayload } from '../types/auth';
declare global {
namespace Express {
interface Request {
user?: JWTPayload;
}
}
}
export const authenticateToken = (
req: Request,
res: Response,
next: NextFunction
): void => {
const authHeader = req.headers['authorization'];
const token = authHeader?.split(' ')[1];
if (!token) {
res.status(401).json({ error: 'No token provided' });
return;
}
try {
const decoded = jwt.verify(
token,
process.env.JWT_SECRET!
) as JWTPayload;
req.user = decoded;
next();
} catch (error) {
res.status(403).json({ error: 'Invalid token' });
}
};
Common Pitfalls and Solutions
Issue: XSS Vulnerabilities
Solution: Never store JWTs in localStorage. Use httpOnly cookies or memory storage.
Issue: Token Expiration Handling
Solution: Implement automatic token refresh before expiration.
Issue: No Way to Revoke Tokens
Solution: Implement token blacklisting or use short-lived tokens with refresh tokens.
Related Articles
- Quick Guide to Linux Process Management and Job Control
- Setting Up Automated Backups with rsync, borgbackup and
- Advanced systemd Service Management and Unit File Creation
- Penetration Testing Reconnaissance
Conclusion
JWT authentication provides a scalable, stateless solution perfect for modern APIs and microservices. By following the security best practices outlined here—using strong secrets, implementing proper expiration, and protecting against common vulnerabilities—you can build a robust authentication system.
Start with the basic implementation, then enhance it with refresh tokens, rate limiting, and proper error handling as your application grows. Remember: security is not a one-time implementation but an ongoing process of monitoring, updating, and improving.
The code examples provided give you a production-ready foundation. Adapt them to your specific needs, always prioritizing security over convenience.
References
[1] Auth0. (2024). JSON Web Token Best Current Practices. Available at: https://auth0.com/docs/secure/tokens/json-web-tokens (Accessed: November 2025)
[2] OWASP Foundation. (2024). Authentication Cheat Sheet. Available at: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html (Accessed: November 2025)
[3] RFC 7519. (2015). JSON Web Token (JWT). Available at: https://datatracker.ietf.org/doc/html/rfc7519 (Accessed: November 2025)