Secure Identity Management in Express.js: From JWT and OAuth 2.0 to RBAC
Security is the backbone of modern web architecture, yet it remains one of the most difficult aspects to implement correctly. This article deconstructs complex authentication flows, guiding you through implementing stateless JWT systems, integrating social logins via Passport.js, and enforcing strict Role-Based Access Control (RBAC) middleware to protect your API endpoints.
Authentication & Authorization
Basic Authentication
Basic Authentication is the simplest HTTP authentication scheme where credentials (username:password) are Base64-encoded and sent in the Authorization header; it should only be used over HTTPS due to its inherent insecurity.
// Manual Basic Auth middleware const basicAuth = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Basic ')) { res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"'); return res.status(401).json({ error: 'Authentication required' }); } // Decode Base64: "Basic dXNlcjpwYXNz" -> "user:pass" const base64Credentials = authHeader.split(' ')[1]; const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); const [username, password] = credentials.split(':'); // Validate credentials (use constant-time comparison in production) if (username === 'admin' && password === 'secret123') { req.user = { username }; return next(); } res.status(401).json({ error: 'Invalid credentials' }); }; // Using express-basic-auth package const basicAuth = require('express-basic-auth'); app.use('/admin', basicAuth({ users: { 'admin': 'supersecret' }, challenge: true, realm: 'Admin Area' })); app.get('/admin/dashboard', (req, res) => { res.json({ message: `Welcome, ${req.auth.user}!` }); });
┌─────────────────────────────────────────────────────────────┐
│ BASIC AUTH FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │ GET /protected │ │
│ │───────────────────────────────────▶│ │
│ │ │ │
│ │ 401 Unauthorized │ │
│ │ WWW-Authenticate: Basic │ │
│ │◀───────────────────────────────────│ │
│ │ │ │
│ │ GET /protected │ │
│ │ Authorization: Basic YWRtaW46cGFz │ │
│ │───────────────────────────────────▶│ │
│ │ │ Decode & Verify │
│ │ 200 OK │ │
│ │◀───────────────────────────────────│ │
│ │
│ ⚠️ HTTPS Required - Base64 is NOT encryption! │
│ │
└─────────────────────────────────────────────────────────────┘
Session-based Authentication
Session-based authentication stores user state on the server (in memory, database, or Redis), issuing a session ID cookie to the client that identifies them on subsequent requests—ideal for traditional web applications with server-rendered views.
const session = require('express-session'); const RedisStore = require('connect-redis').default; const Redis = require('ioredis'); const redisClient = new Redis(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, name: 'sessionId', // Custom cookie name (not 'connect.sid') cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, // Prevents XSS maxAge: 24 * 60 * 60 * 1000, // 24 hours sameSite: 'strict' // CSRF protection } })); // Login app.post('/login', async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user || !await bcrypt.compare(password, user.password)) { return res.status(401).json({ error: 'Invalid credentials' }); } // Store user info in session req.session.userId = user.id; req.session.role = user.role; res.json({ message: 'Logged in successfully' }); }); // Auth middleware const requireAuth = (req, res, next) => { if (!req.session.userId) { return res.status(401).json({ error: 'Please log in' }); } next(); }; // Protected route app.get('/profile', requireAuth, async (req, res) => { const user = await User.findById(req.session.userId); res.json(user); }); // Logout app.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) return res.status(500).json({ error: 'Logout failed' }); res.clearCookie('sessionId'); res.json({ message: 'Logged out' }); }); });
┌─────────────────────────────────────────────────────────────┐
│ SESSION-BASED AUTH FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client Server Session Store │
│ │ │ │ │
│ │ POST /login │ │ │
│ │ {email, password} │ │ │
│ │───────────────────▶│ │ │
│ │ │ Store session │ │
│ │ │────────────────────▶│ │
│ │ │ │ │
│ │ Set-Cookie: │ │ │
│ │ sessionId=abc123 │ │ │
│ │◀───────────────────│ │ │
│ │ │ │ │
│ │ GET /profile │ │ │
│ │ Cookie: sessionId │ │ │
│ │───────────────────▶│ Lookup session │ │
│ │ │────────────────────▶│ │
│ │ │◀────────────────────│ │
│ │ 200 OK {user} │ │ │
│ │◀───────────────────│ │ │
│ │
└─────────────────────────────────────────────────────────────┘
JWT Authentication
JWT (JSON Web Token) is a stateless authentication mechanism where the server signs a token containing user claims, and the client includes this token in subsequent requests—perfect for APIs and distributed systems that can't share session state.
const jwt = require('jsonwebtoken'); const JWT_SECRET = process.env.JWT_SECRET; const JWT_EXPIRES = '15m'; const REFRESH_SECRET = process.env.REFRESH_SECRET; const REFRESH_EXPIRES = '7d'; // Generate tokens const generateTokens = (user) => ({ accessToken: jwt.sign( { userId: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES } ), refreshToken: jwt.sign( { userId: user.id, tokenVersion: user.tokenVersion }, REFRESH_SECRET, { expiresIn: REFRESH_EXPIRES } ) }); // Login app.post('/login', async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user || !await bcrypt.compare(password, user.password)) { return res.status(401).json({ error: 'Invalid credentials' }); } const tokens = generateTokens(user); // Set refresh token as HTTP-only cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 }); res.json({ accessToken: tokens.accessToken }); }); // Auth middleware const authenticateJWT = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token expired' }); } return res.status(403).json({ error: 'Invalid token' }); } }; // Protected route app.get('/profile', authenticateJWT, (req, res) => { res.json({ user: req.user }); });
┌─────────────────────────────────────────────────────────────┐
│ JWT STRUCTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.signature │
│ ─────────────────────┬─────────────────┬───────── │
│ HEADER │ PAYLOAD │ SIGNATURE │
│ │ │ │
│ { │ { │ HMACSHA256( │
│ "alg": "HS256", │ "userId": 1, │ header + "." + │
│ "typ": "JWT" │ "role": "a", │ payload, │
│ } │ "exp": 1699 │ secret │
│ │ } │ ) │
│ │
├─────────────────────────────────────────────────────────────┤
│ JWT AUTH FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ POST /login │ │
│ │─────────────────────────────────────▶│ │
│ │ │ Create JWT │
│ │ { accessToken: "eyJ..." } │ │
│ │◀─────────────────────────────────────│ │
│ │ │ │
│ │ GET /api/data │ │
│ │ Authorization: Bearer eyJ... │ │
│ │─────────────────────────────────────▶│ │
│ │ │ Verify & decode │
│ │ 200 OK { data } │ │
│ │◀─────────────────────────────────────│ │
│ │
│ ⚠️ Stateless - No server session storage needed │
│ │
└─────────────────────────────────────────────────────────────┘
Passport.js Integration
Passport.js is the de-facto authentication middleware for Express, providing a modular architecture with 500+ strategies (local, OAuth, JWT, etc.) that can be plugged in with minimal configuration while keeping your route handlers clean.
const passport = require('passport'); const LocalStrategy = require('passport-local').Strategy; const JwtStrategy = require('passport-jwt').Strategy; const { ExtractJwt } = require('passport-jwt'); // Initialize Passport app.use(passport.initialize()); // Local Strategy (username/password) passport.use(new LocalStrategy( { usernameField: 'email' }, async (email, password, done) => { try { const user = await User.findOne({ email }); if (!user) { return done(null, false, { message: 'User not found' }); } if (!await bcrypt.compare(password, user.password)) { return done(null, false, { message: 'Wrong password' }); } return done(null, user); } catch (err) { return done(err); } } )); // JWT Strategy passport.use(new JwtStrategy( { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET }, async (payload, done) => { try { const user = await User.findById(payload.userId); if (!user) return done(null, false); return done(null, user); } catch (err) { return done(err); } } )); // Session serialization (if using sessions) passport.serializeUser((user, done) => done(null, user.id)); passport.deserializeUser(async (id, done) => { const user = await User.findById(id); done(null, user); }); // Routes app.post('/login', passport.authenticate('local', { session: false }), (req, res) => { const token = jwt.sign({ userId: req.user.id }, JWT_SECRET); res.json({ token }); } ); app.get('/profile', passport.authenticate('jwt', { session: false }), (req, res) => { res.json(req.user); } );
OAuth 2.0 (Google, GitHub, Facebook)
OAuth 2.0 enables users to authenticate using third-party providers, delegating credential management to trusted services like Google or GitHub while your app receives a token to access user profile information.
const GoogleStrategy = require('passport-google-oauth20').Strategy; const GitHubStrategy = require('passport-github2').Strategy; // Google OAuth passport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: '/auth/google/callback' }, async (accessToken, refreshToken, profile, done) => { try { // Find or create user let user = await User.findOne({ googleId: profile.id }); if (!user) { user = await User.create({ googleId: profile.id, email: profile.emails[0].value, name: profile.displayName, avatar: profile.photos[0].value }); } return done(null, user); } catch (err) { return done(err); } } )); // GitHub OAuth passport.use(new GitHubStrategy({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: '/auth/github/callback' }, async (accessToken, refreshToken, profile, done) => { let user = await User.findOne({ githubId: profile.id }); if (!user) { user = await User.create({ githubId: profile.id, email: profile.emails?.[0]?.value, name: profile.displayName, username: profile.username }); } return done(null, user); } )); // Routes app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }) ); app.get('/auth/google/callback', passport.authenticate('google', { session: false }), (req, res) => { const token = generateToken(req.user); res.redirect(`/auth-success?token=${token}`); } ); app.get('/auth/github', passport.authenticate('github', { scope: ['user:email'] }) ); app.get('/auth/github/callback', passport.authenticate('github', { session: false }), (req, res) => { const token = generateToken(req.user); res.redirect(`/auth-success?token=${token}`); } );
┌─────────────────────────────────────────────────────────────┐
│ OAUTH 2.0 FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ User Your App Google/GitHub │
│ │ │ │ │
│ │ Click Login │ │ │
│ │─────────────▶│ │ │
│ │ │ │ │
│ │◀─────────────│ Redirect to OAuth│ │
│ │──────────────────────────────────────────▶│ │
│ │ │ │ │ │
│ │ │ User logs in & consents │ │
│ │ │ │ │ │
│ │◀──────────────────────────────────────────│ │
│ │ Redirect with auth code │ │
│ │─────────────▶│ │ │
│ │ │ Exchange code │ │
│ │ │─────────────────▶│ │
│ │ │◀─────────────────│ │
│ │ │ Access token + │ │
│ │ │ user profile │ │
│ │◀─────────────│ │ │
│ │ JWT Token │ │ │
│ │
└─────────────────────────────────────────────────────────────┘
Role-based Access Control (RBAC)
RBAC restricts system access based on user roles, where each role has specific permissions, allowing fine-grained control over what actions different user types can perform within your application.
// Define roles and permissions const ROLES = { ADMIN: 'admin', MANAGER: 'manager', USER: 'user', GUEST: 'guest' }; const PERMISSIONS = { [ROLES.ADMIN]: ['create', 'read', 'update', 'delete', 'manage_users'], [ROLES.MANAGER]: ['create', 'read', 'update', 'delete'], [ROLES.USER]: ['create', 'read', 'update'], [ROLES.GUEST]: ['read'] }; // Role-based middleware const requireRole = (...allowedRoles) => { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Authentication required' }); } if (!allowedRoles.includes(req.user.role)) { return res.status(403).json({ error: 'Insufficient permissions', required: allowedRoles, current: req.user.role }); } next(); }; }; // Permission-based middleware const requirePermission = (permission) => { return (req, res, next) => { const userPermissions = PERMISSIONS[req.user.role] || []; if (!userPermissions.includes(permission)) { return res.status(403).json({ error: `Permission '${permission}' required` }); } next(); }; }; // Usage app.get('/users', authenticateJWT, requireRole(ROLES.ADMIN, ROLES.MANAGER), getUsers ); app.delete('/users/:id', authenticateJWT, requirePermission('manage_users'), deleteUser ); app.post('/posts', authenticateJWT, requirePermission('create'), createPost );
┌─────────────────────────────────────────────────────────────┐
│ RBAC HIERARCHY │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ ADMIN │ │
│ │─────────│ │
│ │ ALL │ │
│ └────┬────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ MANAGER │ │ EDITOR │ │
│ │─────────│ │─────────│ │
│ │ CRUD │ │ CRU │ │
│ └────┬────┘ └────┬────┘ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────┐ │
│ │ USER │ │
│ │─────────│ │
│ │ Read │ │
│ │ Own data│ │
│ └────┬────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ GUEST │ │
│ │─────────│ │
│ │ Read │ │
│ │ Public │ │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Permission Middleware
Permission middleware provides granular access control by checking if a user has specific permissions for resources, often combining role checks with resource ownership verification for complete authorization logic.
// Advanced permission system class PermissionService { constructor() { this.permissions = new Map(); } define(resource, actions) { this.permissions.set(resource, actions); } can(user, action, resource, resourceData = null) { // Admin bypass if (user.role === 'admin') return true; // Check role permissions const rolePerms = ROLE_PERMISSIONS[user.role] || {}; const resourcePerms = rolePerms[resource] || []; if (!resourcePerms.includes(action)) return false; // Check ownership if required if (resourceData && resourceData.ownerId) { if (action !== 'read' && resourceData.ownerId !== user.id) { return rolePerms[resource]?.includes(`${action}:any`); } } return true; } } const permissionService = new PermissionService(); // Middleware factory const authorize = (action, resource) => { return async (req, res, next) => { // Get resource data if needed let resourceData = null; if (req.params.id) { resourceData = await getResource(resource, req.params.id); if (!resourceData) { return res.status(404).json({ error: 'Resource not found' }); } req.resource = resourceData; } if (!permissionService.can(req.user, action, resource, resourceData)) { return res.status(403).json({ error: 'Forbidden', action, resource }); } next(); }; }; // Usage app.get('/posts/:id', authenticateJWT, authorize('read', 'posts'), (req, res) => res.json(req.resource) ); app.put('/posts/:id', authenticateJWT, authorize('update', 'posts'), // Checks ownership updatePost ); app.delete('/posts/:id', authenticateJWT, authorize('delete', 'posts'), deletePost ); // Combine multiple permissions const requireAll = (...middlewares) => { return async (req, res, next) => { for (const middleware of middlewares) { const result = await new Promise((resolve) => { middleware(req, res, (err) => resolve(err)); }); if (result) return next(result); } next(); }; };
Refresh Tokens
Refresh tokens are long-lived tokens used to obtain new access tokens without requiring the user to re-authenticate, enabling short-lived access tokens for security while maintaining a seamless user experience.
// Token configuration const ACCESS_TOKEN_EXPIRY = '15m'; const REFRESH_TOKEN_EXPIRY = '7d'; // Store refresh tokens (use Redis in production) const refreshTokens = new Map(); app.post('/login', async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user || !await bcrypt.compare(password, user.password)) { return res.status(401).json({ error: 'Invalid credentials' }); } const accessToken = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY } ); const refreshToken = jwt.sign( { userId: user.id, tokenVersion: user.tokenVersion }, process.env.REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY } ); // Store refresh token refreshTokens.set(refreshToken, user.id); // Send refresh token as HTTP-only cookie res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 }); res.json({ accessToken, expiresIn: 900 }); }); app.post('/refresh', async (req, res) => { const { refreshToken } = req.cookies; if (!refreshToken || !refreshTokens.has(refreshToken)) { return res.status(401).json({ error: 'Invalid refresh token' }); } try { const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET); const user = await User.findById(decoded.userId); // Check token version (allows invalidating all tokens) if (user.tokenVersion !== decoded.tokenVersion) { return res.status(401).json({ error: 'Token revoked' }); } const newAccessToken = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY } ); res.json({ accessToken: newAccessToken, expiresIn: 900 }); } catch (err) { return res.status(401).json({ error: 'Invalid refresh token' }); } }); app.post('/logout', (req, res) => { const { refreshToken } = req.cookies; refreshTokens.delete(refreshToken); res.clearCookie('refreshToken'); res.json({ message: 'Logged out' }); }); // Revoke all sessions (increment token version) app.post('/logout-all', authenticateJWT, async (req, res) => { await User.findByIdAndUpdate(req.user.userId, { $inc: { tokenVersion: 1 } }); res.json({ message: 'All sessions revoked' }); });
┌─────────────────────────────────────────────────────────────┐
│ REFRESH TOKEN FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ Timeline ────────────────────────────────────────────────▶ │
│ │
│ Access Token ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ (15 min) │ │
│ │ Expired │
│ ▼ │
│ API Request ─────────────────X 401 Unauthorized │
│ │ │
│ ▼ │
│ POST /refresh ───────────────┬─────────────────────────── │
│ (with refresh token cookie) │ │
│ ▼ │
│ New Access Token ████████████████░░░░░░░░░░░ │
│ │
│ Refresh Token ████████████████████████████████████████░ │
│ (7 days) │ │
│ │ │
│ Expired│ │
│ ▼ │
│ Re-login needed │
│ │
└─────────────────────────────────────────────────────────────┘
Password Hashing (bcrypt)
Bcrypt is a password hashing algorithm that incorporates a salt and configurable work factor, making it computationally expensive to brute-force and resistant to rainbow table attacks, essential for storing user passwords securely.
const bcrypt = require('bcrypt'); const SALT_ROUNDS = 12; // Higher = more secure but slower // Hashing password during registration app.post('/register', async (req, res) => { const { email, password } = req.body; // Validate password strength if (password.length < 8) { return res.status(400).json({ error: 'Password too short' }); } // Hash password const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); const user = await User.create({ email, password: hashedPassword // Store hash, never plain text }); res.status(201).json({ message: 'User created', userId: user.id }); }); // Verifying password during login app.post('/login', async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user) { // Use same response to prevent user enumeration return res.status(401).json({ error: 'Invalid credentials' }); } // Compare provided password with stored hash const isValid = await bcrypt.compare(password, user.password); if (!isValid) { return res.status(401).json({ error: 'Invalid credentials' }); } // Generate token... res.json({ message: 'Login successful' }); }); // Mongoose pre-save hook for automatic hashing const userSchema = new mongoose.Schema({ email: String, password: String }); userSchema.pre('save', async function(next) { // Only hash if password is modified if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, SALT_ROUNDS); next(); }); userSchema.methods.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password); };
┌─────────────────────────────────────────────────────────────┐
│ BCRYPT STRUCTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.RQhHGn5Wq2 │
│ ─┬──┬──┬──────────────────────┬────────────────────────── │
│ │ │ │ │ │
│ │ │ │ └─ Hash (31 chars) │
│ │ │ └─ Salt (22 chars) │
│ │ └─ Cost factor (2^12 = 4096 iterations) │
│ └─ Algorithm version (2b) │
│ │
├─────────────────────────────────────────────────────────────┤
│ COST FACTOR GUIDE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Cost │ Iterations │ Time (approx) │ Use Case │
│ ─────┼────────────┼───────────────┼───────────────────── │
│ 10 │ 1,024 │ ~100ms │ Development │
│ 12 │ 4,096 │ ~300ms │ Production (default) │
│ 14 │ 16,384 │ ~1s │ High security │
│ │
│ ⚠️ Hash during registration is slow by design! │
│ │
└─────────────────────────────────────────────────────────────┘
Two-factor Authentication
Two-factor authentication (2FA) adds an extra security layer by requiring both something the user knows (password) and something they have (phone/authenticator app), typically using TOTP (Time-based One-Time Passwords).
const speakeasy = require('speakeasy'); const QRCode = require('qrcode'); // Enable 2FA - Step 1: Generate secret app.post('/2fa/setup', authenticateJWT, async (req, res) => { const secret = speakeasy.generateSecret({ name: `MyApp:${req.user.email}`, issuer: 'MyApp' }); // Temporarily store secret (not verified yet) await User.findByIdAndUpdate(req.user.userId, { twoFactorTempSecret: secret.base32 }); // Generate QR code for authenticator app const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url); res.json({ secret: secret.base32, // Backup code qrCode: qrCodeUrl }); }); // Enable 2FA - Step 2: Verify and activate app.post('/2fa/verify', authenticateJWT, async (req, res) => { const { token } = req.body; const user = await User.findById(req.user.userId); const verified = speakeasy.totp.verify({ secret: user.twoFactorTempSecret, encoding: 'base32', token, window: 1 // Allow 1 period tolerance }); if (!verified) { return res.status(400).json({ error: 'Invalid token' }); } // Activate 2FA await User.findByIdAndUpdate(req.user.userId, { twoFactorEnabled: true, twoFactorSecret: user.twoFactorTempSecret, $unset: { twoFactorTempSecret: 1 } }); // Generate backup codes const backupCodes = Array.from({ length: 10 }, () => crypto.randomBytes(4).toString('hex') ); await User.findByIdAndUpdate(req.user.userId, { backupCodes: backupCodes.map(code => bcrypt.hashSync(code, 10)) }); res.json({ message: '2FA enabled', backupCodes // Show once, user must save }); }); // Login with 2FA app.post('/login', async (req, res) => { const { email, password, totpToken } = req.body; const user = await User.findOne({ email }); if (!user || !await bcrypt.compare(password, user.password)) { return res.status(401).json({ error: 'Invalid credentials' }); } // Check if 2FA is enabled if (user.twoFactorEnabled) { if (!totpToken) { return res.status(200).json({ requires2FA: true, message: 'Please provide 2FA token' }); } const verified = speakeasy.totp.verify({ secret: user.twoFactorSecret, encoding: 'base32', token: totpToken, window: 1 }); if (!verified) { return res.status(401).json({ error: 'Invalid 2FA token' }); } } const token = generateToken(user); res.json({ accessToken: token }); });
┌─────────────────────────────────────────────────────────────┐ │ 2FA FLOW │ ├─────────────────────────────────────────────────────────────┤ │ │ │ SETUP: │ │ ┌────────┐ ┌────────┐ ┌────────────────┐ │ │ │ Server │───▶│ QR Code│───▶│ Authenticator │ │ │ │ │ │ │ │ App (scan) │ │ │ └────────┘ └────────┘ └────────────────┘ │ │ │ │ LOGIN: │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │Password│───▶│ Valid? │───▶│ 2FA? │───▶│ Verify │ │ │ └────────┘ └────┬───┘ └───┬────┘ │ TOTP │ │ │ │ │ └───┬────┘ │ │ │ No │ Yes │ │ │ ▼ ▼ ▼ │ │ ┌─────┐ ┌─────┐ ┌─────────┐ │ │ │Fail │ │Token│ │ Success │ │ │ └─────┘ └─────┘ └─────────┘ │ │ │ │ TOTP: Time-based One-Time Password │ │ - 6-digit code │ │ - Changes every 30 seconds │ │ - Based on shared secret + current time │ │ │ └─────────────────────────────────────────────────────────────┘