← Back to Tutorials
Node.js

Build a Token-Based Authentication System

Difficulty: Intermediate Est. Time: ~3 hours

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