Build a Token-Based Authentication System
Introduction
Token-based authentication is the standard for modern web applications. Tokens are stateless, scalable, and can be used across different domains. In this tutorial, we'll build a complete token authentication system with access and refresh tokens.
What You'll Build
- Access token generation
- Refresh token mechanism
- Token validation middleware
- Token revocation system
Core Concepts
Access Tokens
Short-lived tokens that authorize API requests. Typically valid for 15-60 minutes.
Refresh Tokens
Long-lived tokens used to obtain new access tokens without re-authentication.
Prerequisites
- Node.js 14+
- npm install jsonwebtoken
Token Manager
Create auth/token-manager.js:
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class TokenManager {
constructor(options = {}) {
this.accessTokenSecret = options.accessTokenSecret || 'access-secret';
this.refreshTokenSecret = options.refreshTokenSecret || 'refresh-secret';
this.accessTokenExpiry = options.accessTokenExpiry || '15m';
this.refreshTokenExpiry = options.refreshTokenExpiry || '7d';
this.refreshTokens = new Map();
this.revokedTokens = new Set();
}
generateAccessToken(user) {
const payload = {
userId: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, this.accessTokenSecret, {
expiresIn: this.accessTokenExpiry,
issuer: 'myapp'
});
}
generateRefreshToken(user) {
const payload = {
userId: user.id,
type: 'refresh'
};
const token = jwt.sign(payload, this.refreshTokenSecret, {
expiresIn: this.refreshTokenExpiry,
issuer: 'myapp'
});
this.refreshTokens.set(token, {
userId: user.id,
createdAt: Date.now()
});
return token;
}
generateTokenPair(user) {
return {
accessToken: this.generateAccessToken(user),
refreshToken: this.generateRefreshToken(user),
expiresIn: this._parseExpiry(this.accessTokenExpiry)
};
}
verifyAccessToken(token) {
try {
if (this.revokedTokens.has(token)) {
return { valid: false, error: 'Token revoked' };
}
const decoded = jwt.verify(token, this.accessTokenSecret, {
issuer: 'myapp'
});
return { valid: true, payload: decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}
verifyRefreshToken(token) {
try {
const decoded = jwt.verify(token, this.refreshTokenSecret, {
issuer: 'myapp'
});
if (!this.refreshTokens.has(token)) {
return { valid: false, error: 'Token not found' };
}
return { valid: true, payload: decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}
refreshAccessToken(refreshToken) {
const result = this.verifyRefreshToken(refreshToken);
if (!result.valid) {
return null;
}
const user = { id: result.payload.userId };
return this.generateAccessToken(user);
}
revokeToken(token) {
this.revokedTokens.add(token);
}
revokeRefreshToken(token) {
this.refreshTokens.delete(token);
}
revokeAllUserTokens(userId) {
for (const [token, data] of this.refreshTokens) {
if (data.userId === userId) {
this.refreshTokens.delete(token);
}
}
}
_parseExpiry(expiry) {
const match = expiry.match(/(\d+)([mhds])/);
if (match) {
const value = parseInt(match[1]);
const unit = match[2];
return value * { m: 60, h: 3600, d: 86400, s: 1 }[unit];
}
return 900;
}
}
module.exports = TokenManager;
Token Validator
class TokenValidator {
constructor(tokenManager) {
this.tokenManager = tokenManager;
}
validateAccessToken(token) {
return this.tokenManager.verifyAccessToken(token);
}
extractTokenFromHeader(authHeader) {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
return authHeader.substring(7);
}
extractUserFromRequest(req) {
const authHeader = req.headers.authorization;
const token = this.extractTokenFromHeader(authHeader);
if (!token) {
return { authenticated: false };
}
const result = this.tokenManager.verifyAccessToken(token);
if (!result.valid) {
return { authenticated: false, error: result.error };
}
return {
authenticated: true,
user: result.payload
};
}
requireAuth() {
return (req, res, next) => {
const authResult = this.extractUserFromRequest(req);
if (!authResult.authenticated) {
return res.status(401).json({
error: 'Authentication required'
});
}
req.user = authResult.user;
next();
};
}
requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
}
}
module.exports = TokenValidator;
Express Middleware
const TokenManager = require('./token-manager');
const TokenValidator = require('./token-validator');
const tokenManager = new TokenManager({
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET,
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET
});
const validator = new TokenValidator(tokenManager);
function login(req, res) {
const { email, password } = req.body;
const user = findUserByEmail(email);
if (!user || !validatePassword(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = tokenManager.generateTokenPair(user);
res.json(tokens);
}
function refresh(req, res) {
const { refreshToken } = req.body;
const newAccessToken = tokenManager.refreshAccessToken(refreshToken);
if (!newAccessToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
res.json({ accessToken: newAccessToken });
}
function logout(req, res) {
const authHeader = req.headers.authorization;
const token = validator.extractTokenFromHeader(authHeader);
if (token) {
tokenManager.revokeToken(token);
}
res.json({ message: 'Logged out successfully' });
}
module.exports = {
tokenManager,
validator,
login,
refresh,
logout,
requireAuth: validator.requireAuth.bind(validator),
requireRole: validator.requireRole.bind(validator)
};
Testing
const request = require('supertest');
const app = require('./app');
describe('Authentication API', () => {
let accessToken;
it('should login successfully', async () => {
const res = await request(app)
.post('/auth/login')
.send({ email: 'user@example.com', password: 'password123' });
expect(res.status).toBe(200);
expect(res.body.accessToken).toBeDefined();
expect(res.body.refreshToken).toBeDefined();
accessToken = res.body.accessToken;
});
it('should access protected route', async () => {
const res = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${accessToken}`);
expect(res.status).toBe(200);
});
it('should refresh token', async () => {
const res = await request(app)
.post('/auth/refresh')
.send({ refreshToken });
expect(res.status).toBe(200);
expect(res.body.accessToken).toBeDefined();
});
});
Summary
You built a complete token-based authentication system with access and refresh tokens, validation, and role-based authorization.
Possible Extensions
- Add token rotation
- Implement token blacklisting
- Add device fingerprinting