⏱️4 min read · 785 words

How to Implement JWT Authentication in Node.js / Express (Full Example 2026)
Question: How do I implement JWT (JSON Web Token) authentication in a Node.js/Express app with protected routes?
📋 Table of Contents
Setup
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon
# .env
PORT=5000
JWT_SECRET=your-super-secret-key-min-32-chars-long-here
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
Project Structure
src/
├── middleware/
│ └── auth.js # JWT verification middleware
├── routes/
│ ├── auth.js # Login/register routes
│ └── protected.js # Protected routes
├── utils/
│ └── jwt.js # JWT helper functions
└── server.js
JWT Utilities
// src/utils/jwt.js
const jwt = require('jsonwebtoken');
const generateAccessToken = (userId) => {
return jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
};
const generateRefreshToken = (userId) => {
return jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
};
const verifyToken = (token) => {
return jwt.verify(token, process.env.JWT_SECRET);
};
module.exports = { generateAccessToken, generateRefreshToken, verifyToken };
Auth Middleware
// src/middleware/auth.js
const { verifyToken } = require('../utils/jwt');
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = verifyToken(token);
if (decoded.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
req.userId = decoded.userId;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
module.exports = { authenticate };
Auth Routes (Login + Register)
// src/routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const { generateAccessToken, generateRefreshToken, verifyToken } = require('../utils/jwt');
const router = express.Router();
// In-memory user store (replace with database)
const users = [];
// Register
router.post('/register', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const existingUser = users.find(u => u.email === email);
if (existingUser) {
return res.status(409).json({ error: 'User already exists' });
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = { id: Date.now().toString(), email, password: hashedPassword };
users.push(user);
const accessToken = generateAccessToken(user.id);
const refreshToken = generateRefreshToken(user.id);
res.status(201).json({
message: 'User created',
accessToken,
refreshToken
});
});
// Login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = generateAccessToken(user.id);
const refreshToken = generateRefreshToken(user.id);
res.json({ accessToken, refreshToken });
});
// Refresh token
router.post('/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const decoded = verifyToken(refreshToken);
if (decoded.type !== 'refresh') {
return res.status(401).json({ error: 'Invalid token type' });
}
const newAccessToken = generateAccessToken(decoded.userId);
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Invalid or expired refresh token' });
}
});
module.exports = router;
Protected Routes
// src/routes/protected.js
const express = require('express');
const { authenticate } = require('../middleware/auth');
const router = express.Router();
// All routes in this file require authentication
router.use(authenticate);
router.get('/profile', (req, res) => {
res.json({
message: 'Protected route accessed',
userId: req.userId
});
});
router.get('/dashboard', (req, res) => {
res.json({ data: 'Dashboard data', userId: req.userId });
});
module.exports = router;
Server Setup
// src/server.js
require('dotenv').config();
const express = require('express');
const authRoutes = require('./routes/auth');
const protectedRoutes = require('./routes/protected');
const app = express();
app.use(express.json());
app.use('/auth', authRoutes);
app.use('/api', protectedRoutes);
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
Testing the API
# Register
curl -X POST http://localhost:5000/auth/register -H "Content-Type: application/json" -d '{"email":"user@example.com","password":"SecurePass123!"}'
# Login - get tokens
curl -X POST http://localhost:5000/auth/login -H "Content-Type: application/json" -d '{"email":"user@example.com","password":"SecurePass123!"}'
# Access protected route
curl http://localhost:5000/api/profile -H "Authorization: Bearer YOUR_ACCESS_TOKEN_HERE"
# Refresh expired token
curl -X POST http://localhost:5000/auth/refresh -H "Content-Type: application/json" -d '{"refreshToken":"YOUR_REFRESH_TOKEN_HERE"}'
Security Best Practices
- Short-lived access tokens — 15 minutes is standard. Reduces exposure window.
- Long-lived refresh tokens — 7-30 days. Store securely (httpOnly cookie preferred).
- Strong JWT secret — Minimum 32 characters, randomly generated
- Never store tokens in localStorage for sensitive apps — Use httpOnly cookies for refresh tokens
- Implement refresh token rotation — Issue new refresh token on each refresh
- Maintain a token blacklist for logout — Redis-based in production
httpOnly Cookie Pattern (More Secure)
// Store refresh token in httpOnly cookie instead of response body
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // JS can't read this
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
});
res.json({ accessToken }); // Only return access token in body
📚 You might also like
🔗 Share this article



✍️ Leave a Comment