Authentication & Authorization - 1/2
From Security Foundations to Identity Management
You’ve mastered input validation that stops injection attacks, implemented cryptographic security with bulletproof password hashing, and built operational defenses that protect against DDoS attacks and compliance violations. Your applications are secure at every layer—input, cryptographic, and operational. But here’s the identity reality that determines whether users can actually access your secure application: even with perfect security foundations, your application is worthless if users can’t prove who they are or if the system can’t determine what they’re allowed to do.
The authentication nightmare that locks out legitimate users:
// Your perfectly secured application with bulletproof defenses
app.post(
"/api/dashboard",
inputValidation, // Perfect input validation
rateLimiting, // DDoS protection
csrfProtection, // CSRF tokens
async (req, res) => {
try {
// But who is this user? How do we know they should access this?
const userDashboard = await getDashboardData(req.user?.id);
res.json({ dashboard: userDashboard });
// Problems without proper authentication/authorization:
// - req.user is undefined - no authentication system
// - No way to verify user identity
// - No control over who accesses what resources
// - Every endpoint requires custom identity checking
// - Session management is non-existent
// - Password resets are impossible
// - Social login is a pipe dream
} catch (error) {
res.status(500).json({ error: "Dashboard failed" });
}
}
);
// User experience without proper auth:
// - "Why do I have to re-enter my password every page refresh?"
// - "I can't log in with my Google account like everywhere else"
// - "The app forgot who I was after closing my browser"
// - "I can see admin features but I'm just a regular user"
// - "There's no way to reset my password"
The uncomfortable identity truth: Security without identity is like building a fortress with no doors—perfectly secure but completely inaccessible. Modern applications require sophisticated identity systems that seamlessly authenticate users, maintain sessions across devices, integrate with third-party providers, and enforce granular permissions without frustrating legitimate users.
Real-world authentication failure consequences:
// What happens when authentication systems fail:
const authenticationFailureImpact = {
userExperience: {
problem: "Users can't log in during Black Friday sale",
impact: "$2.8M in lost revenue in 4 hours",
cause: "Authentication service overwhelmed, no fallback",
solution: "Required complete system rebuild",
},
securityBreach: {
problem: "Weak JWT implementation allows token manipulation",
impact: "Users can access any account by modifying token claims",
scope: "450,000 user accounts compromised",
compliance: "GDPR fine of $3.2M for inadequate access controls",
},
operationalChaos: {
problem: "No role-based access control",
impact: "Junior developer accidentally deletes production database",
cause: "Everyone has admin access, no permission boundaries",
recovery: "6 days downtime, 72 hours from backups",
},
// Perfect security means nothing if users can't securely access your app
// or if access controls don't prevent unauthorized actions
};
Authentication and authorization mastery requires understanding:
- Authentication vs. authorization fundamentals and how they work together to secure applications
- Session-based authentication with secure session management and cross-device persistence
- JWT token-based authentication with proper security considerations and token lifecycle management
- OAuth 2.0 and OpenID Connect for third-party authentication and social login integration
- Multi-factor authentication (MFA) that balances security with user experience
This article transforms you from security defender to identity architect. You’ll build authentication systems that users love, authorization frameworks that scale with your organization, and identity integrations that work seamlessly across platforms and devices.
Authentication vs. Authorization: The Foundation of Identity
Understanding the Critical Distinction
The identity concepts that developers confuse:
// ❌ Common confusion between authentication and authorization
const handleUserRequest = async (req, res) => {
// This is NOT authentication or authorization - it's just checking if data exists
const user = await db.users.findOne({ email: req.body.email });
if (user) {
// Dangerous assumption: existence = authentication = authorization
const adminData = await getAdminData(); // No verification of identity or permissions
res.json({ user, adminData });
}
// Problems with this approach:
// - No verification that the person making the request IS this user
// - No check that this user SHOULD access admin data
// - Anyone who knows an email address gets full access
// - No session management or token validation
// - No permission boundaries or role checking
};
// The confusion that destroys security:
// Authentication ≠ "Does this user exist in our database?"
// Authorization ≠ "This user exists so they can do anything"
Professional authentication and authorization separation:
// ✅ Clear separation of authentication and authorization concerns
class IdentityManager {
constructor() {
this.sessionStore = new SessionStore();
this.permissionEngine = new PermissionEngine();
this.auditLogger = new AuditLogger();
}
// AUTHENTICATION: Verify WHO the user is
async authenticate(credentials, authMethod = "password") {
try {
let authResult = null;
switch (authMethod) {
case "password":
authResult = await this.authenticateWithPassword(credentials);
break;
case "token":
authResult = await this.authenticateWithToken(credentials);
break;
case "oauth":
authResult = await this.authenticateWithOAuth(credentials);
break;
case "mfa":
authResult = await this.authenticateWithMFA(credentials);
break;
default:
throw new AuthenticationError("Unsupported authentication method");
}
if (!authResult.success) {
await this.logAuthenticationFailure(credentials, authMethod);
throw new AuthenticationError(authResult.reason);
}
// Authentication successful - we now know WHO the user is
const authenticatedUser = {
id: authResult.user.id,
email: authResult.user.email,
username: authResult.user.username,
authenticatedAt: new Date(),
authMethod,
sessionId: authResult.sessionId,
};
await this.logAuthenticationSuccess(authenticatedUser);
return authenticatedUser;
} catch (error) {
console.error("Authentication failed:", error);
throw error;
}
}
// AUTHORIZATION: Verify WHAT the authenticated user can do
async authorize(authenticatedUser, resource, action, context = {}) {
try {
// First verify the user is still authenticated
const isValidSession = await this.validateSession(
authenticatedUser.sessionId
);
if (!isValidSession) {
throw new AuthorizationError(
"Session expired - reauthentication required"
);
}
// Get user's current permissions (may have changed since authentication)
const userPermissions = await this.getUserPermissions(
authenticatedUser.id
);
// Check if user has permission for this specific resource and action
const hasPermission = await this.permissionEngine.checkPermission({
userId: authenticatedUser.id,
resource,
action,
permissions: userPermissions,
context,
});
if (!hasPermission.granted) {
await this.logAuthorizationFailure({
userId: authenticatedUser.id,
resource,
action,
reason: hasPermission.reason,
context,
});
throw new AuthorizationError(`Access denied: ${hasPermission.reason}`);
}
// Authorization successful - user can perform this action
const authorizationResult = {
userId: authenticatedUser.id,
resource,
action,
granted: true,
grantedAt: new Date(),
permissions: hasPermission.matchedPermissions,
context,
};
await this.logAuthorizationSuccess(authorizationResult);
return authorizationResult;
} catch (error) {
console.error("Authorization failed:", error);
throw error;
}
}
// Combined middleware for routes that need both
requireAuth(resource, action) {
return async (req, res, next) => {
try {
// Step 1: Authenticate - determine WHO the user is
const authenticatedUser = await this.extractAuthenticatedUser(req);
if (!authenticatedUser) {
return res.status(401).json({
error: "Authentication required",
message: "You must be logged in to access this resource",
});
}
// Step 2: Authorize - determine WHAT they can do
const authorizationResult = await this.authorize(
authenticatedUser,
resource,
action,
{
ip: req.ip,
userAgent: req.get("User-Agent"),
path: req.path,
method: req.method,
}
);
// Attach identity information to request
req.user = authenticatedUser;
req.permissions = authorizationResult.permissions;
req.authContext = authorizationResult;
next();
} catch (error) {
if (error instanceof AuthenticationError) {
return res.status(401).json({
error: "Authentication failed",
message: error.message,
});
}
if (error instanceof AuthorizationError) {
return res.status(403).json({
error: "Access denied",
message: error.message,
});
}
console.error("Auth middleware failed:", error);
res.status(500).json({
error: "Authentication system error",
message: "Unable to verify your identity",
});
}
};
}
async extractAuthenticatedUser(req) {
// Try multiple authentication methods
// Method 1: Session-based authentication
const sessionId = req.cookies?.sessionId;
if (sessionId) {
const sessionData = await this.sessionStore.getSession(sessionId);
if (sessionData && !sessionData.expired) {
return sessionData.user;
}
}
// Method 2: JWT token authentication
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith("Bearer ")) {
const token = authHeader.slice(7);
const tokenData = await this.validateJWTToken(token);
if (tokenData) {
return tokenData.user;
}
}
// Method 3: API key authentication
const apiKey = req.headers["x-api-key"];
if (apiKey) {
const apiKeyData = await this.validateApiKey(apiKey);
if (apiKeyData) {
return apiKeyData.user;
}
}
return null; // No valid authentication found
}
}
// Real-world usage with clear separation
const identityManager = new IdentityManager();
// Public endpoint - no authentication or authorization required
app.get("/api/public/status", (req, res) => {
res.json({ status: "operational", timestamp: new Date() });
});
// Authentication required, no specific authorization
app.get(
"/api/user/profile",
identityManager.requireAuth("user:profile", "read"),
async (req, res) => {
// We know WHO the user is (req.user)
// We know they can READ their profile (authorized)
const profile = await getUserProfile(req.user.id);
res.json({ profile });
}
);
// Both authentication and specific authorization required
app.delete(
"/api/admin/users/:userId",
identityManager.requireAuth("admin:users", "delete"),
async (req, res) => {
// We know WHO is making the request (req.user)
// We know they have DELETE permission on admin:users (authorized)
await deleteUser(req.params.userId, req.user.id);
res.json({ success: true });
}
);
// Resource-specific authorization
app.get(
"/api/projects/:projectId",
identityManager.requireAuth("project", "read"),
async (req, res) => {
// Authorization engine checks if THIS user can read THIS specific project
const project = await getProject(req.params.projectId);
res.json({ project });
}
);
Session-Based Authentication: Traditional but Robust
Secure Session Management Implementation
The session challenge that scales poorly:
// ❌ Naive session implementation that breaks in production
const sessions = new Map(); // In-memory storage - loses data on restart
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = await validateCredentials(email, password);
if (user) {
const sessionId = Math.random().toString(36); // Weak session ID
sessions.set(sessionId, { userId: user.id }); // No expiration
res.cookie("sessionId", sessionId); // No security flags
res.json({ success: true });
}
});
// Problems with this approach:
// - Sessions lost on server restart
// - Weak session ID generation
// - No session expiration or cleanup
// - No CSRF protection
// - No secure cookie configuration
// - Doesn't scale across multiple servers
// - No session invalidation on logout
Professional session-based authentication:
// ✅ Production-ready session management system
const Redis = require("redis");
const crypto = require("crypto");
class SecureSessionManager {
constructor() {
this.redis = Redis.createClient(process.env.REDIS_URL);
this.sessionPrefix = "session:";
this.userSessionsPrefix = "user_sessions:";
this.sessionTimeout = 24 * 60 * 60 * 1000; // 24 hours
this.slidingSessionWindow = 2 * 60 * 60 * 1000; // 2 hours sliding window
this.setupSessionCleanup();
}
// Generate cryptographically secure session ID
generateSessionId() {
return crypto.randomBytes(32).toString("hex");
}
async createSession(userId, metadata = {}) {
try {
const sessionId = this.generateSessionId();
const expiresAt = new Date(Date.now() + this.sessionTimeout);
const sessionData = {
userId,
sessionId,
createdAt: new Date(),
expiresAt,
lastActivityAt: new Date(),
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
deviceFingerprint: metadata.deviceFingerprint,
isActive: true,
loginMethod: metadata.loginMethod || "password",
mfaVerified: metadata.mfaVerified || false,
};
// Store session data
await this.redis.setex(
`${this.sessionPrefix}${sessionId}`,
Math.floor(this.sessionTimeout / 1000),
JSON.stringify(sessionData)
);
// Track user's active sessions (for security monitoring)
await this.addToUserSessions(userId, sessionId);
// Log session creation
this.logSessionEvent({
type: "session_created",
sessionId,
userId,
...metadata,
});
return {
sessionId,
expiresAt,
user: await this.getUserData(userId),
};
} catch (error) {
console.error("Session creation failed:", error);
throw new Error("Failed to create session");
}
}
async validateSession(sessionId) {
try {
if (!sessionId) return null;
const sessionData = await this.redis.get(
`${this.sessionPrefix}${sessionId}`
);
if (!sessionData) return null;
const session = JSON.parse(sessionData);
// Check if session is expired
if (new Date() > new Date(session.expiresAt)) {
await this.destroySession(sessionId);
return null;
}
// Check if session needs activity refresh (sliding window)
const timeSinceLastActivity =
Date.now() - new Date(session.lastActivityAt).getTime();
if (timeSinceLastActivity > this.slidingSessionWindow) {
// Update last activity and extend expiration
session.lastActivityAt = new Date();
session.expiresAt = new Date(Date.now() + this.sessionTimeout);
await this.redis.setex(
`${this.sessionPrefix}${sessionId}`,
Math.floor(this.sessionTimeout / 1000),
JSON.stringify(session)
);
}
return session;
} catch (error) {
console.error("Session validation failed:", error);
return null;
}
}
async destroySession(sessionId) {
try {
// Get session data before destroying
const sessionData = await this.redis.get(
`${this.sessionPrefix}${sessionId}`
);
if (sessionData) {
const session = JSON.parse(sessionData);
// Remove from user's session list
await this.removeFromUserSessions(session.userId, sessionId);
// Log session destruction
this.logSessionEvent({
type: "session_destroyed",
sessionId,
userId: session.userId,
reason: "explicit_logout",
});
}
// Delete session data
await this.redis.del(`${this.sessionPrefix}${sessionId}`);
return true;
} catch (error) {
console.error("Session destruction failed:", error);
return false;
}
}
async destroyAllUserSessions(userId, exceptSessionId = null) {
try {
const userSessionsKey = `${this.userSessionsPrefix}${userId}`;
const sessionIds = await this.redis.smembers(userSessionsKey);
const destroyPromises = sessionIds
.filter((sessionId) => sessionId !== exceptSessionId)
.map((sessionId) => this.destroySession(sessionId));
await Promise.all(destroyPromises);
this.logSessionEvent({
type: "all_sessions_destroyed",
userId,
sessionCount: sessionIds.length,
exceptSessionId,
});
return sessionIds.length;
} catch (error) {
console.error("Failed to destroy all user sessions:", error);
throw error;
}
}
async addToUserSessions(userId, sessionId) {
const userSessionsKey = `${this.userSessionsPrefix}${userId}`;
await this.redis.sadd(userSessionsKey, sessionId);
// Set expiration on user sessions list
await this.redis.expire(
userSessionsKey,
Math.floor(this.sessionTimeout / 1000)
);
}
async removeFromUserSessions(userId, sessionId) {
const userSessionsKey = `${this.userSessionsPrefix}${userId}`;
await this.redis.srem(userSessionsKey, sessionId);
}
async getUserActiveSessions(userId) {
try {
const userSessionsKey = `${this.userSessionsPrefix}${userId}`;
const sessionIds = await this.redis.smembers(userSessionsKey);
const sessionPromises = sessionIds.map(async (sessionId) => {
const sessionData = await this.redis.get(
`${this.sessionPrefix}${sessionId}`
);
return sessionData ? JSON.parse(sessionData) : null;
});
const sessions = await Promise.all(sessionPromises);
// Filter out null sessions (expired/invalid)
return sessions.filter((session) => session !== null);
} catch (error) {
console.error("Failed to get user active sessions:", error);
return [];
}
}
setupSessionCleanup() {
// Clean up expired sessions every hour
setInterval(async () => {
await this.cleanupExpiredSessions();
}, 60 * 60 * 1000);
}
async cleanupExpiredSessions() {
try {
console.log("Starting expired session cleanup");
// This would be more efficient with Redis SCAN in production
const sessionKeys = await this.redis.keys(`${this.sessionPrefix}*`);
let cleanedCount = 0;
for (const key of sessionKeys) {
const sessionData = await this.redis.get(key);
if (sessionData) {
const session = JSON.parse(sessionData);
if (new Date() > new Date(session.expiresAt)) {
await this.redis.del(key);
cleanedCount++;
}
}
}
console.log(`Cleaned up ${cleanedCount} expired sessions`);
} catch (error) {
console.error("Session cleanup failed:", error);
}
}
// Security monitoring
async detectSuspiciousActivity(userId, newSessionMetadata) {
try {
const activeSessions = await this.getUserActiveSessions(userId);
const suspiciousSignals = [];
// Check for sessions from multiple countries
const uniqueCountries = new Set();
activeSessions.forEach((session) => {
if (session.country) uniqueCountries.add(session.country);
});
if (uniqueCountries.size > 2) {
suspiciousSignals.push("multiple_countries");
}
// Check for unusual device fingerprints
const uniqueDevices = new Set();
activeSessions.forEach((session) => {
if (session.deviceFingerprint)
uniqueDevices.add(session.deviceFingerprint);
});
if (uniqueDevices.size > 5) {
suspiciousSignals.push("multiple_devices");
}
// Check for concurrent sessions from different IPs
const recentSessions = activeSessions.filter(
(session) =>
Date.now() - new Date(session.lastActivityAt).getTime() <
5 * 60 * 1000 // 5 minutes
);
const recentIPs = new Set(recentSessions.map((s) => s.ipAddress));
if (recentIPs.size > 3) {
suspiciousSignals.push("concurrent_different_ips");
}
if (suspiciousSignals.length > 0) {
await this.alertSuspiciousActivity({
userId,
signals: suspiciousSignals,
activeSessions: activeSessions.length,
newSession: newSessionMetadata,
});
}
return suspiciousSignals;
} catch (error) {
console.error("Suspicious activity detection failed:", error);
return [];
}
}
}
// Authentication service using secure sessions
class SessionAuthService {
constructor() {
this.sessionManager = new SecureSessionManager();
this.passwordManager = new SecurePasswordManager(); // From previous blog
}
async login(credentials, metadata = {}) {
try {
const { email, password } = credentials;
// Authenticate user
const user = await this.authenticateUser(email, password);
if (!user) {
throw new AuthenticationError("Invalid credentials");
}
// Check for suspicious activity
const suspiciousSignals =
await this.sessionManager.detectSuspiciousActivity(user.id, metadata);
if (suspiciousSignals.length > 0) {
// Require additional verification for suspicious logins
return {
success: false,
requiresAdditionalVerification: true,
verificationMethods: ["email", "sms"],
suspiciousSignals,
};
}
// Create session
const sessionResult = await this.sessionManager.createSession(user.id, {
...metadata,
loginMethod: "password",
});
return {
success: true,
session: sessionResult,
user: sessionResult.user,
};
} catch (error) {
console.error("Login failed:", error);
throw error;
}
}
async logout(sessionId) {
try {
const destroyed = await this.sessionManager.destroySession(sessionId);
return { success: destroyed };
} catch (error) {
console.error("Logout failed:", error);
throw error;
}
}
async logoutAllDevices(userId, currentSessionId) {
try {
const destroyedCount = await this.sessionManager.destroyAllUserSessions(
userId,
currentSessionId
);
return {
success: true,
destroyedSessions: destroyedCount,
};
} catch (error) {
console.error("Logout all devices failed:", error);
throw error;
}
}
// Session-based authentication middleware
requireSession() {
return async (req, res, next) => {
try {
const sessionId = req.cookies?.sessionId;
if (!sessionId) {
return res.status(401).json({
error: "No session",
message: "Please log in to continue",
});
}
const session = await this.sessionManager.validateSession(sessionId);
if (!session) {
// Clear invalid session cookie
res.clearCookie("sessionId");
return res.status(401).json({
error: "Invalid session",
message: "Your session has expired. Please log in again.",
});
}
// Get full user data
const user = await this.getUserData(session.userId);
if (!user || !user.isActive) {
await this.sessionManager.destroySession(sessionId);
res.clearCookie("sessionId");
return res.status(401).json({
error: "User not active",
message: "Your account is no longer active",
});
}
// Attach session and user to request
req.session = session;
req.user = user;
next();
} catch (error) {
console.error("Session middleware failed:", error);
res.status(500).json({
error: "Session validation failed",
message: "Unable to validate your session",
});
}
};
}
}
// Usage in routes
const sessionAuth = new SessionAuthService();
app.post("/api/login", async (req, res) => {
try {
const result = await sessionAuth.login(req.body, {
ipAddress: req.ip,
userAgent: req.get("User-Agent"),
deviceFingerprint: req.headers["x-device-fingerprint"],
});
if (result.success) {
// Set secure session cookie
res.cookie("sessionId", result.session.sessionId, {
httpOnly: true, // Prevent XSS access
secure: process.env.NODE_ENV === "production", // HTTPS only in production
sameSite: "strict", // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
});
res.json({
success: true,
user: result.user,
expiresAt: result.session.expiresAt,
});
} else {
res.status(202).json(result); // 202 Accepted, additional verification required
}
} catch (error) {
res.status(401).json({
error: "Login failed",
message: error.message,
});
}
});
app.post("/api/logout", sessionAuth.requireSession(), async (req, res) => {
const sessionId = req.cookies.sessionId;
const result = await sessionAuth.logout(sessionId);
res.clearCookie("sessionId");
res.json(result);
});
app.get(
"/api/user/sessions",
sessionAuth.requireSession(),
async (req, res) => {
const sessions = await sessionAuth.sessionManager.getUserActiveSessions(
req.user.id
);
res.json({ sessions });
}
);
JWT Token-Based Authentication: Stateless and Scalable
Professional JWT Implementation with Security
The JWT security pitfalls that destroy trust:
// ❌ Insecure JWT implementation that gets hacked
const jwt = require("jsonwebtoken");
app.post("/login", async (req, res) => {
const user = await validateCredentials(req.body.email, req.body.password);
if (user) {
// DANGER: Weak JWT implementation
const token = jwt.sign(
{
userId: user.id,
role: "user", // Attacker can change this to 'admin'
},
"secret123" // Weak secret, easily cracked
// No expiration - token valid forever
);
res.json({ token });
}
});
// Vulnerable validation
app.use((req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (token) {
try {
req.user = jwt.verify(token, "secret123"); // Same weak secret
// No additional validation - accepts any valid-looking JWT
} catch (error) {
// Silently fails, continues without authentication
}
}
next();
});
// Attack vectors:
// 1. Brute force the weak secret
// 2. Modify token claims (change role to admin)
// 3. Use tokens forever (no expiration)
// 4. No blacklisting for compromised tokens
// 5. No refresh token mechanism
Production-ready JWT authentication system:
// ✅ Secure JWT implementation with comprehensive security
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
class SecureJWTManager {
constructor() {
// Strong secrets from environment
this.accessTokenSecret = process.env.JWT_ACCESS_SECRET;
this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
if (!this.accessTokenSecret || !this.refreshTokenSecret) {
throw new Error("JWT secrets not configured");
}
// Token configuration
this.accessTokenExpiry = "15m"; // Short-lived access tokens
this.refreshTokenExpiry = "7d"; // Longer-lived refresh tokens
this.issuer = "your-app";
this.audience = "your-app-users";
// Token blacklist (Redis-based)
this.tokenBlacklist = new Map(); // Use Redis in production
this.setupTokenCleanup();
}
async generateTokenPair(user) {
try {
const tokenId = crypto.randomUUID(); // Unique token identifier
const issuedAt = Math.floor(Date.now() / 1000);
// Access token payload (minimal, frequently verified)
const accessPayload = {
sub: user.id, // Subject (user ID)
email: user.email,
username: user.username,
role: user.role,
permissions: user.permissions || [],
iat: issuedAt,
jti: tokenId, // JWT ID for blacklisting
type: "access",
};
// Refresh token payload (more detailed, less frequently used)
const refreshPayload = {
sub: user.id,
email: user.email,
iat: issuedAt,
jti: crypto.randomUUID(),
type: "refresh",
// Store device/session info for security
deviceFingerprint: user.deviceFingerprint,
ipAddress: user.ipAddress,
};
// Generate tokens
const accessToken = jwt.sign(accessPayload, this.accessTokenSecret, {
expiresIn: this.accessTokenExpiry,
issuer: this.issuer,
audience: this.audience,
algorithm: "HS256",
});
const refreshToken = jwt.sign(refreshPayload, this.refreshTokenSecret, {
expiresIn: this.refreshTokenExpiry,
issuer: this.issuer,
audience: this.audience,
algorithm: "HS256",
});
// Store refresh token metadata for security tracking
await this.storeRefreshTokenMetadata(refreshPayload.jti, {
userId: user.id,
deviceFingerprint: user.deviceFingerprint,
ipAddress: user.ipAddress,
userAgent: user.userAgent,
createdAt: new Date(),
isActive: true,
});
return {
accessToken,
refreshToken,
accessTokenExpiresIn: this.parseExpiryToSeconds(this.accessTokenExpiry),
refreshTokenExpiresIn: this.parseExpiryToSeconds(
this.refreshTokenExpiry
),
tokenType: "Bearer",
};
} catch (error) {
console.error("Token generation failed:", error);
throw new Error("Failed to generate authentication tokens");
}
}
async verifyAccessToken(token) {
try {
// First check if token is blacklisted
if (await this.isTokenBlacklisted(token)) {
throw new Error("Token has been revoked");
}
// Verify token signature and claims
const decoded = jwt.verify(token, this.accessTokenSecret, {
issuer: this.issuer,
audience: this.audience,
algorithms: ["HS256"],
});
// Validate token type
if (decoded.type !== "access") {
throw new Error("Invalid token type");
}
// Additional security checks
const securityChecks = await this.performSecurityChecks(decoded);
if (!securityChecks.valid) {
throw new Error(securityChecks.reason);
}
return {
valid: true,
user: {
id: decoded.sub,
email: decoded.email,
username: decoded.username,
role: decoded.role,
permissions: decoded.permissions,
},
tokenId: decoded.jti,
issuedAt: new Date(decoded.iat * 1000),
};
} catch (error) {
console.error("Access token verification failed:", error);
return {
valid: false,
error: error.message,
};
}
}
async refreshAccessToken(refreshToken) {
try {
// Verify refresh token
const decoded = jwt.verify(refreshToken, this.refreshTokenSecret, {
issuer: this.issuer,
audience: this.audience,
algorithms: ["HS256"],
});
if (decoded.type !== "refresh") {
throw new Error("Invalid refresh token type");
}
// Check if refresh token is still active
const refreshTokenMetadata = await this.getRefreshTokenMetadata(
decoded.jti
);
if (!refreshTokenMetadata || !refreshTokenMetadata.isActive) {
throw new Error("Refresh token has been revoked");
}
// Get current user data (permissions may have changed)
const currentUser = await this.getCurrentUserData(decoded.sub);
if (!currentUser || !currentUser.isActive) {
throw new Error("User account is not active");
}
// Generate new access token with current user data
const newTokenPair = await this.generateTokenPair({
...currentUser,
deviceFingerprint: refreshTokenMetadata.deviceFingerprint,
ipAddress: refreshTokenMetadata.ipAddress,
});
return {
success: true,
accessToken: newTokenPair.accessToken,
accessTokenExpiresIn: newTokenPair.accessTokenExpiresIn,
};
} catch (error) {
console.error("Token refresh failed:", error);
return {
success: false,
error: error.message,
};
}
}
async revokeToken(token, tokenType = "access") {
try {
let decoded;
if (tokenType === "access") {
decoded = jwt.decode(token); // Don't verify expired tokens
} else {
decoded = jwt.verify(token, this.refreshTokenSecret);
}
if (!decoded) {
throw new Error("Invalid token format");
}
// Add token to blacklist
await this.blacklistToken(decoded.jti, decoded.exp);
// If refreshing token, deactivate it
if (tokenType === "refresh") {
await this.deactivateRefreshToken(decoded.jti);
}
this.logTokenRevocation({
tokenId: decoded.jti,
userId: decoded.sub,
tokenType,
revokedAt: new Date(),
});
return { success: true };
} catch (error) {
console.error("Token revocation failed:", error);
return { success: false, error: error.message };
}
}
async revokeAllUserTokens(userId) {
try {
// Get all active refresh tokens for user
const refreshTokens = await this.getUserRefreshTokens(userId);
// Deactivate all refresh tokens
const revokePromises = refreshTokens.map((token) =>
this.deactivateRefreshToken(token.jti)
);
await Promise.all(revokePromises);
this.logTokenRevocation({
userId,
tokenType: "all",
count: refreshTokens.length,
revokedAt: new Date(),
});
return {
success: true,
revokedTokens: refreshTokens.length,
};
} catch (error) {
console.error("Bulk token revocation failed:", error);
return { success: false, error: error.message };
}
}
// Security checks for additional validation
async performSecurityChecks(tokenPayload) {
// Check if user account is still active
const user = await this.getCurrentUserData(tokenPayload.sub);
if (!user || !user.isActive) {
return { valid: false, reason: "User account inactive" };
}
// Check if user's role/permissions have changed significantly
if (user.role !== tokenPayload.role) {
return {
valid: false,
reason: "User role changed - reauthentication required",
};
}
// Check token age vs security requirements
const tokenAge = Date.now() - tokenPayload.iat * 1000;
const maxTokenAge = 24 * 60 * 60 * 1000; // 24 hours max
if (tokenAge > maxTokenAge) {
return { valid: false, reason: "Token too old" };
}
return { valid: true };
}
// JWT authentication middleware
requireJWT(requiredPermissions = []) {
return async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({
error: "No access token",
message: "Authorization header with Bearer token required",
});
}
const token = authHeader.slice(7);
const verificationResult = await this.verifyAccessToken(token);
if (!verificationResult.valid) {
return res.status(401).json({
error: "Invalid token",
message: verificationResult.error,
});
}
// Check permissions if required
if (requiredPermissions.length > 0) {
const userPermissions = verificationResult.user.permissions || [];
const hasPermission = requiredPermissions.some(
(permission) =>
userPermissions.includes(permission) ||
userPermissions.includes("admin")
);
if (!hasPermission) {
return res.status(403).json({
error: "Insufficient permissions",
message: `Required permissions: ${requiredPermissions.join(
", "
)}`,
userPermissions,
});
}
}
// Attach user to request
req.user = verificationResult.user;
req.token = {
id: verificationResult.tokenId,
issuedAt: verificationResult.issuedAt,
};
next();
} catch (error) {
console.error("JWT middleware failed:", error);
res.status(500).json({
error: "Token validation failed",
message: "Unable to validate your access token",
});
}
};
}
parseExpiryToSeconds(expiry) {
// Convert JWT expiry format to seconds
const units = {
s: 1,
m: 60,
h: 3600,
d: 86400,
};
const match = expiry.match(/^(\d+)([smhd])$/);
if (match) {
return parseInt(match[1]) * units[match[2]];
}
return 3600; // Default 1 hour
}
}
// Usage in authentication routes
const jwtManager = new SecureJWTManager();
app.post("/api/auth/login", async (req, res) => {
try {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({
error: "Authentication failed",
message: "Invalid email or password",
});
}
const tokenPair = await jwtManager.generateTokenPair({
...user,
deviceFingerprint: req.headers["x-device-fingerprint"],
ipAddress: req.ip,
userAgent: req.get("User-Agent"),
});
// Set refresh token as httpOnly cookie
res.cookie("refreshToken", tokenPair.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: tokenPair.refreshTokenExpiresIn * 1000,
});
res.json({
success: true,
accessToken: tokenPair.accessToken,
tokenType: tokenPair.tokenType,
expiresIn: tokenPair.accessTokenExpiresIn,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
},
});
} catch (error) {
console.error("Login failed:", error);
res.status(500).json({
error: "Login failed",
message: "Unable to complete authentication",
});
}
});
app.post("/api/auth/refresh", async (req, res) => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({
error: "No refresh token",
message: "Please log in again",
});
}
const result = await jwtManager.refreshAccessToken(refreshToken);
if (!result.success) {
res.clearCookie("refreshToken");
return res.status(401).json({
error: "Token refresh failed",
message: result.error,
});
}
res.json({
success: true,
accessToken: result.accessToken,
tokenType: "Bearer",
expiresIn: result.accessTokenExpiresIn,
});
} catch (error) {
console.error("Token refresh failed:", error);
res.status(500).json({
error: "Token refresh failed",
message: "Unable to refresh your session",
});
}
});
app.post("/api/auth/logout", jwtManager.requireJWT(), async (req, res) => {
try {
const accessToken = req.headers.authorization.slice(7);
const refreshToken = req.cookies.refreshToken;
// Revoke both tokens
await Promise.all([
jwtManager.revokeToken(accessToken, "access"),
refreshToken
? jwtManager.revokeToken(refreshToken, "refresh")
: Promise.resolve(),
]);
res.clearCookie("refreshToken");
res.json({ success: true, message: "Logged out successfully" });
} catch (error) {
console.error("Logout failed:", error);
res.status(500).json({
error: "Logout failed",
message: "Unable to complete logout",
});
}
});
// Protected route examples
app.get("/api/user/profile", jwtManager.requireJWT(), async (req, res) => {
const profile = await getUserProfile(req.user.id);
res.json({ profile });
});
app.delete(
"/api/admin/users/:userId",
jwtManager.requireJWT(["admin", "user:delete"]),
async (req, res) => {
await deleteUser(req.params.userId);
res.json({ success: true });
}
);
OAuth 2.0 and OpenID Connect: Third-Party Authentication
Professional OAuth Implementation
OAuth 2.0 integration for seamless user experience:
// ✅ Comprehensive OAuth 2.0 and OpenID Connect implementation
const axios = require("axios");
const crypto = require("crypto");
class OAuthManager {
constructor() {
this.providers = {
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: process.env.GOOGLE_REDIRECT_URI,
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
scopes: ["openid", "email", "profile"],
},
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
redirectUri: process.env.GITHUB_REDIRECT_URI,
authUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
scopes: ["user:email"],
},
microsoft: {
clientId: process.env.MICROSOFT_CLIENT_ID,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
redirectUri: process.env.MICROSOFT_REDIRECT_URI,
authUrl:
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
userInfoUrl: "https://graph.microsoft.com/v1.0/me",
scopes: ["openid", "email", "profile"],
},
};
this.stateStore = new Map(); // Use Redis in production
this.setupStateCleanup();
}
generateAuthUrl(provider, state = null) {
try {
const providerConfig = this.providers[provider];
if (!providerConfig) {
throw new Error(`Unsupported OAuth provider: ${provider}`);
}
// Generate secure state parameter to prevent CSRF
const stateValue = state || crypto.randomBytes(32).toString("hex");
const stateExpiry = Date.now() + 10 * 60 * 1000; // 10 minutes
this.stateStore.set(stateValue, {
provider,
createdAt: Date.now(),
expiresAt: stateExpiry,
});
const params = new URLSearchParams({
client_id: providerConfig.clientId,
redirect_uri: providerConfig.redirectUri,
scope: providerConfig.scopes.join(" "),
response_type: "code",
state: stateValue,
// Additional security parameters
nonce: crypto.randomBytes(16).toString("hex"),
});
return {
authUrl: `${providerConfig.authUrl}?${params.toString()}`,
state: stateValue,
};
} catch (error) {
console.error("OAuth URL generation failed:", error);
throw error;
}
}
async handleCallback(provider, code, state) {
try {
// Validate state parameter
if (!this.validateState(state, provider)) {
throw new Error("Invalid state parameter - possible CSRF attack");
}
// Exchange code for tokens
const tokenResponse = await this.exchangeCodeForTokens(provider, code);
// Get user information
const userInfo = await this.getUserInfo(
provider,
tokenResponse.access_token
);
// Process user data and handle account linking
const processedUser = await this.processOAuthUser(
provider,
userInfo,
tokenResponse
);
// Clean up state
this.stateStore.delete(state);
return {
success: true,
user: processedUser,
tokens: tokenResponse,
};
} catch (error) {
console.error("OAuth callback failed:", error);
throw error;
}
}
validateState(state, expectedProvider) {
if (!state) return false;
const stateData = this.stateStore.get(state);
if (!stateData) return false;
// Check expiration
if (Date.now() > stateData.expiresAt) {
this.stateStore.delete(state);
return false;
}
// Check provider match
if (stateData.provider !== expectedProvider) {
return false;
}
return true;
}
async exchangeCodeForTokens(provider, code) {
try {
const providerConfig = this.providers[provider];
const tokenRequest = {
client_id: providerConfig.clientId,
client_secret: providerConfig.clientSecret,
code,
grant_type: "authorization_code",
redirect_uri: providerConfig.redirectUri,
};
const response = await axios.post(providerConfig.tokenUrl, tokenRequest, {
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
});
if (response.data.error) {
throw new Error(
`Token exchange failed: ${response.data.error_description}`
);
}
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
id_token: response.data.id_token, // OpenID Connect
token_type: response.data.token_type,
expires_in: response.data.expires_in,
scope: response.data.scope,
};
} catch (error) {
console.error("Token exchange failed:", error);
throw new Error("Failed to exchange authorization code for tokens");
}
}
async getUserInfo(provider, accessToken) {
try {
const providerConfig = this.providers[provider];
const response = await axios.get(providerConfig.userInfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
return response.data;
} catch (error) {
console.error("User info fetch failed:", error);
throw new Error("Failed to fetch user information");
}
}
async processOAuthUser(provider, oauthUserInfo, tokens) {
try {
// Normalize user data across providers
const normalizedUser = this.normalizeUserData(provider, oauthUserInfo);
// Check if user already exists
const existingUser = await this.findUserByEmail(normalizedUser.email);
if (existingUser) {
// Link OAuth account to existing user
await this.linkOAuthAccount(existingUser.id, provider, {
providerId: normalizedUser.providerId,
providerData: oauthUserInfo,
tokens,
});
return {
...existingUser,
isNewUser: false,
linkedProvider: provider,
};
} else {
// Create new user from OAuth data
const newUser = await this.createUserFromOAuth(
provider,
normalizedUser,
tokens
);
return {
...newUser,
isNewUser: true,
primaryProvider: provider,
};
}
} catch (error) {
console.error("OAuth user processing failed:", error);
throw error;
}
}
normalizeUserData(provider, oauthData) {
switch (provider) {
case "google":
return {
providerId: oauthData.id,
email: oauthData.email,
firstName: oauthData.given_name,
lastName: oauthData.family_name,
fullName: oauthData.name,
profilePicture: oauthData.picture,
emailVerified: oauthData.email_verified,
locale: oauthData.locale,
};
case "github":
return {
providerId: oauthData.id.toString(),
email: oauthData.email,
firstName: oauthData.name?.split(" ")[0],
lastName: oauthData.name?.split(" ").slice(1).join(" "),
fullName: oauthData.name,
username: oauthData.login,
profilePicture: oauthData.avatar_url,
bio: oauthData.bio,
company: oauthData.company,
location: oauthData.location,
};
case "microsoft":
return {
providerId: oauthData.id,
email: oauthData.mail || oauthData.userPrincipalName,
firstName: oauthData.givenName,
lastName: oauthData.surname,
fullName: oauthData.displayName,
jobTitle: oauthData.jobTitle,
officeLocation: oauthData.officeLocation,
};
default:
throw new Error(
`Normalization not implemented for provider: ${provider}`
);
}
}
async createUserFromOAuth(provider, normalizedUser, tokens) {
try {
const userId = crypto.randomUUID();
const newUser = {
id: userId,
email: normalizedUser.email,
username: normalizedUser.username || normalizedUser.email.split("@")[0],
firstName: normalizedUser.firstName,
lastName: normalizedUser.lastName,
fullName: normalizedUser.fullName,
profilePicture: normalizedUser.profilePicture,
emailVerified: normalizedUser.emailVerified || false,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
authMethod: "oauth",
primaryOAuthProvider: provider,
};
// Create user account
await db.users.insertOne(newUser);
// Create OAuth account link
await this.linkOAuthAccount(userId, provider, {
providerId: normalizedUser.providerId,
providerData: normalizedUser,
tokens,
isPrimary: true,
});
// Log OAuth account creation
this.logOAuthEvent({
type: "account_created",
userId,
provider,
email: normalizedUser.email,
timestamp: new Date(),
});
return newUser;
} catch (error) {
console.error("OAuth user creation failed:", error);
throw error;
}
}
async linkOAuthAccount(userId, provider, oauthData) {
try {
const linkData = {
userId,
provider,
providerId: oauthData.providerId,
providerData: oauthData.providerData,
accessToken: this.encryptToken(oauthData.tokens.access_token),
refreshToken: oauthData.tokens.refresh_token
? this.encryptToken(oauthData.tokens.refresh_token)
: null,
tokenExpiresAt: oauthData.tokens.expires_in
? new Date(Date.now() + oauthData.tokens.expires_in * 1000)
: null,
isPrimary: oauthData.isPrimary || false,
createdAt: new Date(),
updatedAt: new Date(),
};
// Check if link already exists
const existingLink = await db.oauthAccounts.findOne({
userId,
provider,
providerId: oauthData.providerId,
});
if (existingLink) {
// Update existing link
await db.oauthAccounts.updateOne(
{ _id: existingLink._id },
{ $set: { ...linkData, updatedAt: new Date() } }
);
} else {
// Create new link
await db.oauthAccounts.insertOne(linkData);
}
this.logOAuthEvent({
type: "account_linked",
userId,
provider,
providerId: oauthData.providerId,
timestamp: new Date(),
});
} catch (error) {
console.error("OAuth account linking failed:", error);
throw error;
}
}
}
// OAuth routes
const oauthManager = new OAuthManager();
app.get("/api/auth/oauth/:provider", (req, res) => {
try {
const { provider } = req.params;
const { authUrl, state } = oauthManager.generateAuthUrl(provider);
// Store state in session for additional security
req.session.oauthState = state;
res.redirect(authUrl);
} catch (error) {
console.error("OAuth initiation failed:", error);
res.status(400).json({
error: "OAuth setup failed",
message: error.message,
});
}
});
app.get("/api/auth/oauth/:provider/callback", async (req, res) => {
try {
const { provider } = req.params;
const { code, state, error } = req.query;
if (error) {
return res.status(400).json({
error: "OAuth authorization failed",
message: error,
});
}
if (!code || !state) {
return res.status(400).json({
error: "Invalid callback",
message: "Missing authorization code or state",
});
}
const result = await oauthManager.handleCallback(provider, code, state);
if (result.success) {
// Create session or JWT for the authenticated user
const sessionResult = await sessionAuth.createSession(result.user.id, {
ipAddress: req.ip,
userAgent: req.get("User-Agent"),
loginMethod: `oauth_${provider}`,
});
// Set session cookie
res.cookie("sessionId", sessionResult.sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 24 * 60 * 60 * 1000,
});
// Redirect to success page
res.redirect(
`${process.env.FRONTEND_URL}/auth/success?newUser=${result.user.isNewUser}`
);
} else {
res.redirect(
`${process.env.FRONTEND_URL}/auth/error?reason=oauth_failed`
);
}
} catch (error) {
console.error("OAuth callback failed:", error);
res.redirect(`${process.env.FRONTEND_URL}/auth/error?reason=server_error`);
}
});
Multi-Factor Authentication: Enhanced Security
Professional MFA Implementation
Comprehensive MFA system for critical security:
// ✅ Production-ready Multi-Factor Authentication
const speakeasy = require("speakeasy");
const QRCode = require("qrcode");
const crypto = require("crypto");
class MultiFactorAuthManager {
constructor() {
this.issuer = process.env.APP_NAME || "Your App";
this.mfaTokenExpiry = 5 * 60 * 1000; // 5 minutes
this.backupCodeCount = 10;
this.maxMFAAttempts = 3;
this.setupMFACleanup();
}
// Setup TOTP (Time-based One-Time Password)
async setupTOTP(userId) {
try {
const user = await this.getUserData(userId);
if (!user) {
throw new Error("User not found");
}
// Generate secret for TOTP
const secret = speakeasy.generateSecret({
name: `${this.issuer} (${user.email})`,
issuer: this.issuer,
length: 32,
});
// Generate QR code for authenticator apps
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// Generate backup codes
const backupCodes = this.generateBackupCodes();
// Store MFA setup (encrypted)
const mfaSetup = {
userId,
method: "totp",
secret: this.encryptSecret(secret.base32),
backupCodes: backupCodes.map((code) => this.hashBackupCode(code)),
isVerified: false,
setupAt: new Date(),
backupCodesUsed: [],
};
await db.mfaSetups.insertOne(mfaSetup);
return {
secret: secret.base32,
qrCode: qrCodeUrl,
backupCodes: backupCodes,
manualEntryKey: secret.base32,
};
} catch (error) {
console.error("TOTP setup failed:", error);
throw error;
}
}
async verifyTOTPSetup(userId, token) {
try {
const mfaSetup = await db.mfaSetups.findOne({
userId,
method: "totp",
isVerified: false,
});
if (!mfaSetup) {
throw new Error("No pending TOTP setup found");
}
const decryptedSecret = this.decryptSecret(mfaSetup.secret);
const verified = speakeasy.totp.verify({
secret: decryptedSecret,
encoding: "base32",
token: token,
window: 2, // Allow 2 time steps tolerance
});
if (!verified) {
throw new Error("Invalid TOTP token");
}
// Mark TOTP as verified and activate
await db.mfaSetups.updateOne(
{ _id: mfaSetup._id },
{
$set: {
isVerified: true,
verifiedAt: new Date(),
isActive: true,
},
}
);
// Update user's MFA status
await db.users.updateOne(
{ id: userId },
{
$set: {
mfaEnabled: true,
mfaMethod: "totp",
mfaEnabledAt: new Date(),
},
}
);
this.logMFAEvent({
type: "mfa_enabled",
userId,
method: "totp",
timestamp: new Date(),
});
return { success: true, message: "MFA enabled successfully" };
} catch (error) {
console.error("TOTP verification failed:", error);
throw error;
}
}
async verifyMFA(userId, token, method = "totp") {
try {
const mfaSetup = await db.mfaSetups.findOne({
userId,
method,
isVerified: true,
isActive: true,
});
if (!mfaSetup) {
throw new Error("MFA not configured for this user");
}
// Check for rate limiting
const recentAttempts = await this.getRecentMFAAttempts(userId);
if (recentAttempts >= this.maxMFAAttempts) {
throw new Error("Too many MFA attempts. Please try again later.");
}
let verified = false;
let usedBackupCode = false;
if (method === "totp") {
// Verify TOTP token
const decryptedSecret = this.decryptSecret(mfaSetup.secret);
verified = speakeasy.totp.verify({
secret: decryptedSecret,
encoding: "base32",
token: token,
window: 2,
});
} else if (method === "backup_code") {
// Verify backup code
const hashedToken = this.hashBackupCode(token);
const backupCodeIndex = mfaSetup.backupCodes.indexOf(hashedToken);
if (
backupCodeIndex !== -1 &&
!mfaSetup.backupCodesUsed.includes(hashedToken)
) {
verified = true;
usedBackupCode = true;
// Mark backup code as used
await db.mfaSetups.updateOne(
{ _id: mfaSetup._id },
{
$push: { backupCodesUsed: hashedToken },
}
);
}
}
// Log MFA attempt
this.logMFAAttempt({
userId,
method,
success: verified,
timestamp: new Date(),
usedBackupCode,
});
if (!verified) {
throw new Error("Invalid MFA token");
}
return {
success: true,
method,
usedBackupCode,
remainingBackupCodes: usedBackupCode
? mfaSetup.backupCodes.length - mfaSetup.backupCodesUsed.length - 1
: mfaSetup.backupCodes.length - mfaSetup.backupCodesUsed.length,
};
} catch (error) {
console.error("MFA verification failed:", error);
throw error;
}
}
async requireMFA(userId, action) {
try {
// Check if MFA is required for this action
const mfaRequirements = await this.getMFARequirements(userId, action);
if (!mfaRequirements.required) {
return { required: false };
}
// Generate MFA challenge
const challengeId = crypto.randomUUID();
const expiresAt = new Date(Date.now() + this.mfaTokenExpiry);
const challenge = {
challengeId,
userId,
action,
createdAt: new Date(),
expiresAt,
verified: false,
attempts: 0,
};
await db.mfaChallenges.insertOne(challenge);
return {
required: true,
challengeId,
expiresAt,
availableMethods: mfaRequirements.availableMethods,
};
} catch (error) {
console.error("MFA requirement check failed:", error);
throw error;
}
}
async completeMFAChallenge(challengeId, token, method = "totp") {
try {
const challenge = await db.mfaChallenges.findOne({
challengeId,
verified: false,
expiresAt: { $gt: new Date() },
});
if (!challenge) {
throw new Error("Invalid or expired MFA challenge");
}
// Verify MFA token
const verificationResult = await this.verifyMFA(
challenge.userId,
token,
method
);
if (!verificationResult.success) {
// Increment attempt count
await db.mfaChallenges.updateOne(
{ challengeId },
{ $inc: { attempts: 1 } }
);
throw new Error("MFA verification failed");
}
// Mark challenge as completed
await db.mfaChallenges.updateOne(
{ challengeId },
{
$set: {
verified: true,
completedAt: new Date(),
method,
},
}
);
return {
success: true,
challengeId,
completedAt: new Date(),
};
} catch (error) {
console.error("MFA challenge completion failed:", error);
throw error;
}
}
// MFA middleware for sensitive operations
requireMFAMiddleware(action) {
return async (req, res, next) => {
try {
if (!req.user) {
return res.status(401).json({
error: "Authentication required",
});
}
// Check if user has MFA enabled
const user = await db.users.findOne({ id: req.user.id });
if (!user.mfaEnabled) {
return res.status(403).json({
error: "MFA required",
message:
"Multi-factor authentication must be enabled for this action",
setupUrl: "/api/mfa/setup",
});
}
// Check if MFA is required for this action
const mfaRequirement = await this.requireMFA(req.user.id, action);
if (!mfaRequirement.required) {
return next(); // MFA not required
}
// Check for MFA challenge completion
const challengeId = req.headers["x-mfa-challenge-id"];
if (!challengeId) {
return res.status(403).json({
error: "MFA challenge required",
challenge: mfaRequirement,
});
}
const challenge = await db.mfaChallenges.findOne({
challengeId,
userId: req.user.id,
verified: true,
expiresAt: { $gt: new Date() },
});
if (!challenge) {
return res.status(403).json({
error: "Invalid or expired MFA challenge",
challenge: mfaRequirement,
});
}
// MFA verified, attach challenge info to request
req.mfaChallenge = challenge;
next();
} catch (error) {
console.error("MFA middleware failed:", error);
res.status(500).json({
error: "MFA verification failed",
message: "Unable to verify multi-factor authentication",
});
}
};
}
generateBackupCodes() {
const codes = [];
for (let i = 0; i < this.backupCodeCount; i++) {
const code = crypto.randomInt(100000000, 999999999).toString();
codes.push(code);
}
return codes;
}
hashBackupCode(code) {
return crypto.createHash("sha256").update(code).digest("hex");
}
}
// MFA API routes
const mfaManager = new MultiFactorAuthManager();
app.post(
"/api/mfa/setup/totp",
sessionAuth.requireSession(),
async (req, res) => {
try {
const setup = await mfaManager.setupTOTP(req.user.id);
res.json({
success: true,
setup: {
qrCode: setup.qrCode,
manualEntryKey: setup.manualEntryKey,
backupCodes: setup.backupCodes,
},
});
} catch (error) {
console.error("MFA setup failed:", error);
res.status(500).json({
error: "MFA setup failed",
message: error.message,
});
}
}
);
app.post(
"/api/mfa/verify/setup",
sessionAuth.requireSession(),
async (req, res) => {
try {
const { token } = req.body;
const result = await mfaManager.verifyTOTPSetup(req.user.id, token);
res.json(result);
} catch (error) {
res.status(400).json({
error: "MFA verification failed",
message: error.message,
});
}
}
);
app.post(
"/api/mfa/challenge/:challengeId",
sessionAuth.requireSession(),
async (req, res) => {
try {
const { challengeId } = req.params;
const { token, method } = req.body;
const result = await mfaManager.completeMFAChallenge(
challengeId,
token,
method
);
res.json(result);
} catch (error) {
res.status(400).json({
error: "MFA challenge failed",
message: error.message,
});
}
}
);
// Protected route requiring MFA
app.delete(
"/api/account",
sessionAuth.requireSession(),
mfaManager.requireMFAMiddleware("account_deletion"),
async (req, res) => {
try {
await deleteUserAccount(req.user.id);
res.json({
success: true,
message: "Account deleted successfully",
});
} catch (error) {
res.status(500).json({
error: "Account deletion failed",
message: error.message,
});
}
}
);
Key Takeaways
Authentication and authorization form the identity foundation that determines who can access your secure application and what they can do. Session-based authentication provides robust server-side control, JWT enables stateless scalability, OAuth integrates third-party providers seamlessly, and MFA adds critical security layers for sensitive operations.
The identity management mindset:
- Authentication ≠ Authorization: Know WHO the user is (authentication) separately from WHAT they can do (authorization)
- Security through redundancy: Multiple authentication factors and token validation layers prevent single points of failure
- User experience matters: Seamless identity flows encourage adoption while maintaining security
- Session management scales: Proper session handling enables multi-device, multi-platform user experiences
What distinguishes professional identity systems:
- Clear separation between authentication and authorization with proper middleware architecture
- Session management that handles expiration, cleanup, and security monitoring automatically
- JWT implementations that use proper secrets, expiration, and blacklisting for compromised tokens
- OAuth integrations that support multiple providers with normalized user data handling
- MFA systems that balance security requirements with user experience
What’s Next
This article covered authentication fundamentals, token management, and third-party integration. The next article completes the identity picture with role-based access control (RBAC), permission systems, social login scaling, Single Sign-On (SSO), and authentication best practices that enterprise applications require.
Authentication establishes identity. Authorization controls access. Together, they create the foundation for secure, scalable applications that users trust and administrators can manage effectively across complex organizational structures.