Building a High-Performance MERN Authentication System with JWT, Refresh Tokens & RBAC
Building a High-Performance MERN Authentication System with JWT, Refresh Tokens & RBAC
Stateless authentication is the backbone of modern web applications. In high-concurrency microservices and full-stack MERN (MongoDB, Express, React, Node.js) architectures, naive JWT implementations—such as storing access tokens in localStorage—expose applications to Cross-Site Scripting (XSS) vulnerabilities.
In this deep dive, we will walk through engineering a production-ready, enterprise-grade authentication system featuring HTTP-Only SameSite cookies, short-lived Access Tokens, sliding Refresh Token rotation, and declarative Role-Based Access Control (RBAC).
---
1. Security Threat Model & Token Architecture
Before writing code, let's understand the two primary attack vectors in modern web authentication:
1. **Cross-Site Scripting (XSS):** If an attacker injects a malicious script via an unsafe input, the script can access localStorage and exfiltrate authentication tokens instantly. 2. **Cross-Site Request Forgery (CSRF):** If credentials are stored in cookies, browsers automatically attach them to cross-origin requests unless strict cookie attributes (SameSite=Strict or SameSite=Lax with HttpOnly and Secure) are configured.
The Dual-Token Security Model
To mitigate both vectors, we implement a dual-token strategy:
- **Access Token:** Short lifespan (15 minutes). Signed using HMAC SHA-256 (HS256) or RSA SHA-256 (RS256). Transmitted via Authorization header or memory state. - **Refresh Token:** Longer lifespan (7 days). Stored in a cryptographically random, HTTP-Only, Secure, SameSite Cookie. Stored in Redis or MongoDB with hashed signatures to support instant revocation.
---
2. Express.js Authentication & Refresh Middleware Implementation
Let's inspect the core Node.js middleware for verifying short-lived JWT access tokens and handling sliding session refreshes.
import { Request, Response, NextFunction } from 'express';export interface AuthenticatedRequest extends Request { user?: { id: string; email: string; roles: string[]; }; }
export const authenticateToken = ( req: AuthenticatedRequest, res: Response, next: NextFunction ) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1];
if (!token) { return res.status(401).json({ error: 'Access token required' }); }
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!, (err, user) => { if (err) { return res.status(403).json({ error: 'Invalid or expired access token' }); } req.user = user as AuthenticatedRequest['user']; next(); }); }; ```
Role-Based Access Control (RBAC) Guard
export const authorizeRoles = (...allowedRoles: string[]) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user || !req.user.roles) {
return res.status(403).json({ error: 'Access denied: No role permissions attached' });const hasPermission = req.user.roles.some((role) => allowedRoles.includes(role));
if (!hasPermission) { return res.status(403).json({ error: 'Access denied: Insufficient privileges' }); }
next(); }; }; ```
---
3. Database Schema & Token Revocation with MongoDB
When a user logs out or changes their password, we must invalidate all existing refresh tokens. Here is the Mongoose user schema with refresh token tracking:
const UserSchema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true, index: true }, passwordHash: { type: String, required: true }, roles: { type: [String], default: ['USER'] }, refreshTokenHash: { type: String, default: null }, tokenVersion: { type: Number, default: 0 }, }, { timestamps: true });
export const UserModel = mongoose.model('User', UserSchema); ```
---
4. Performance & Key Takeaways
1. **Zero XSS Token Exposure:** Storing access tokens in memory and refresh tokens in HTTP-only cookies guarantees that malicious third-party scripts cannot extract credentials. 2. **Sub-10ms Token Refresh:** Using Redis or indexed MongoDB queries for token revocation checks ensures negligible authentication overhead. 3. **Seamless UX:** Axios silent token refreshing guarantees users stay logged in without unexpected modal prompts or page reloads.
*Engineered by Sameer Khan — Full Stack Developer & Software Engineer.*
