May 7, 2025 •
11 min readLet's start with a basic JWT authentication system using Node.js
, Express
, and the jsonwebtoken library
. This example includes user login, token generation, and protecting routes.
Install dependencies:
npm init -y
npm install express jsonwebtoken bcrypt
Create a file named server.js and set up a basic Express server.
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
// Simulated user database (replace with a real database in production)
const users = [
{
id: 1,
email: 'user@example.com',
password: '$2b$10$...hashedPassword...' // Hashed password (use bcrypt to generate)
}
];
// Secret key for signing JWTs (store in environment variables in production)
const JWT_SECRET = 'your-secret-key';
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
Allow users to register with an email and password. Passwords are hashed using bcrypt.
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// Check if user already exists
if (users.find(user => user.email === email)) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Save user
const user = { id: users.length + 1, email, password: passwordHash };
users.push(user);
res.status(201).json({ message: 'User registered successfully' });
});
Validate credentials and issue a JWT upon successful login.
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Find user
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '1h' } // Token expires in 1 hour
);
res.json({ token });
});
Create a middleware to verify JWTs and protect routes.
// Middleware to verify JWT
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload; // Attach user data to request
next();
} catch (err) {
res.status(403).json({ message: 'Invalid or expired token' });
}
}
// Protected route
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: `Welcome, user ${req.user.email}!` });
});
Start the server:
node server.js
Register a user:
curl -X POST http://localhost:3000/register -H "Content-Type: application/json" -d '{"email":"john@example.com","password":"password123"}'
Log in to get a JWT:
curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email":"john@example.com","password":"password123"}'
Response: {"token":"eyJhbG..."}
Access the protected route with the JWT:
curl -X GET http://localhost:3000/protected -H "Authorization: Bearer eyJhbG..."
Response: {"message":"Welcome, user john@example.com!"}
Now, let's add features like refresh tokens, role-based authorization, and better security practices to make the system more robust.
JWTs typically have short expiration times (e.g., 1 hour) for security. Refresh tokens allow users to obtain new JWTs without re-entering credentials.
Store refresh tokens in a database (or in-memory for this example).
Issue a refresh token during login.
Create an endpoint to exchange refresh tokens for new JWTs.
// In-memory refresh token store (use a database in production)
const refreshTokens = [];
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate access token
const accessToken = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '15m' } // Short-lived access token
);
// Generate refresh token
const refreshToken = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '7d' } // Long-lived refresh token
);
refreshTokens.push(refreshToken);
res.json({ accessToken, refreshToken });
});
// Refresh token endpoint
app.post('/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
try {
const payload = jwt.verify(refreshToken, JWT_SECRET);
const accessToken = jwt.sign(
{ userId: payload.userId, email: users.find(u => u.id === payload.userId).email },
JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (err) {
res.status(403).json({ message: 'Invalid refresh token' });
}
});
Log in to get accessToken and refreshToken.
After the access token expires (15 minutes), use the refresh token:
curl -X POST http://localhost:3000/refresh -H "Content-Type: application/json" -d '{"refreshToken":"eyJhbG..."}'
Response: {"accessToken":"eyJhbG..."}
Add roles to the JWT payload and enforce access control based on user roles (e.g., admin vs. user).
Modify the user model to include roles.
Add role checks in middleware.
// Updated user model
const users = [
{
id: 1,
email: 'admin@example.com',
password: '$2b$10$...hashedPassword...',
role: 'admin'
},
{
id: 2,
email: 'user@example.com',
password: '$2b$10$...hashedPassword...',
role: 'user'
}
];
// Update login to include role in JWT
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '7d' }
);
refreshTokens.push(refreshToken);
res.json({ accessToken, refreshToken });
});
// Middleware for role-based access
function restrictTo(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Insufficient permissions' });
}
next();
};
}
// Admin-only route
app.get('/admin', authenticateToken, restrictTo('admin'), (req, res) => {
res.json({ message: 'Admin-only content' });
});
npm install dotenv
Create a .env file:
JWT_SECRET=your-secure-secret-key
Update server.js:
require('dotenv').config();
const JWT_SECRET = process.env.JWT_SECRET;
Use HTTP-only cookies instead of local storage to mitigate XSS attacks.
Modify the server to set the token in a cookie:
app.post('/login', async (req, res) => {
// ... (same login logic)
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
res.json({ refreshToken });
});
// Update middleware to read from cookie
function authenticateToken(req, res, next) {
const token = req.cookies.accessToken;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload;
next();
} catch (err) {
res.status(403).json({ message: 'Invalid or expired token' });
}
}
For production systems, you need to handle token revocation (e.g., for logout) and ensure scalability. Let's explore these advanced techniques.
JWTs are stateless and remain valid until they expire, which complicates logout. To revoke tokens, maintain a blacklist of invalidated tokens.
Install Redis and the redis package:
npm install redis
Blacklist tokens on logout and check the blacklist in the middleware.
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });
client.connect();
// Logout endpoint
app.post('/logout', authenticateToken, async (req, res) => {
const token = req.cookies.accessToken;
const payload = req.user;
// Blacklist token until its original expiration
const expiresIn = payload.exp - Math.floor(Date.now() / 1000);
await client.setEx(`blacklist:${token}`, expiresIn, 'blacklisted');
res.clearCookie('accessToken');
res.json({ message: 'Logged out' });
});
// Update middleware to check blacklist
async function authenticateToken(req, res, next) {
const token = req.cookies.accessToken;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
// Check blacklist
const isBlacklisted = await client.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(403).json({ message: 'Token revoked' });
}
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload;
next();
} catch (err) {
res.status(403).json({ message: 'Invalid or expired token' });
}
}
To enhance security, implement refresh token rotation, where a new refresh token is issued with each refresh request, and old ones are invalidated.
Store refresh tokens with a unique ID and track their usage.
const { v4: uuidv4 } = require('uuid'); // npm install uuid
// Update refresh token store to include IDs
const refreshTokenStore = new Map(); // Key: tokenId, Value: { token, userId }
// Update login
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenId: uuidv4() },
JWT_SECRET,
{ expiresIn: '7d' }
);
refreshTokenStore.set(refreshToken.payload.tokenId, { token: refreshToken, userId: user.id });
res.cookie('accessToken', accessToken, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ refreshToken });
});
// Update refresh endpoint
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(403).json({ message: 'No refresh token provided' });
}
try {
const payload = jwt.verify(refreshToken, JWT_SECRET);
const storedToken = refreshTokenStore.get(payload.tokenId);
if (!storedToken || storedToken.token !== refreshToken) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
// Generate new access and refresh tokens
const newAccessToken = jwt.sign(
{ userId: payload.userId, email: users.find(u => u.id === payload.userId).email, role: users.find(u => u.id === payload.userId).role },
JWT_SECRET,
{ expiresIn: '15m' }
);
const newRefreshToken = jwt.sign(
{ userId: payload.userId, tokenId: uuidv4() },
JWT_SECRET,
{ expiresIn: '7d' }
);
// Rotate refresh token
refreshTokenStore.delete(payload.tokenId);
refreshTokenStore.set(newRefreshToken.payload.tokenId, { token: newRefreshToken, userId: payload.userId });
res.cookie('accessToken', newAccessToken, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ refreshToken: newRefreshToken });
} catch (err) {
res.status(403).json({ message: 'Invalid refresh token' });
}
});
Asymmetric Keys: Use RSA or ECDSA instead of HMAC for signing JWTs in production. Store private keys securely and use public keys for verification.
const { publicKey, privateKey } = require('crypto').generateKeyPairSync('rsa', { modulusLength: 2048 });
const token = jwt.sign({ userId: 1 }, privateKey, { algorithm: 'RS256', expiresIn: '1h' });
const payload = jwt.verify(token, publicKey);
Validate Claims: Check iss (issuer), aud (audience), and other claims in the JWT.
Rate Limiting: Use a library like express-rate-limit to prevent brute-force attacks on login endpoints.
CORS: Configure CORS to restrict token usage to trusted origins.
JWT-based authentication is a versatile and scalable solution for securing modern web applications. We started with a simple system for beginners, added refresh tokens and role-based authorization for intermediate users, and explored advanced techniques like token revocation and refresh token rotation for production-grade systems.
Thanks for reading! If you have any questions or need further assistance, feel free to reach out. Happy coding!