Introduction
Modern web applications face sophisticated security threats requiring robust authentication and authorization systems. This comprehensive guide covers OAuth 2.0, JWT implementation, and Zero Trust architecture principles to build secure web applications.
OAuth 2.0 Fundamentals
OAuth 2.0 is an industry-standard authorization framework that enables secure delegated access without exposing user credentials.
Core OAuth 2.0 Flows
Authorization Code Flow (Most Secure)
// Step 1: Redirect user to authorization server
function initiateAuth() {
const params = new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID!,
redirect_uri: 'https://yourapp.com/callback',
response_type: 'code',
scope: 'openid profile email',
state: generateRandomState(), // CSRF protection
code_challenge: generateCodeChallenge(), // PKCE
code_challenge_method: 'S256'
});
window.location.href = `https://auth.provider.com/authorize?${params}`;
}
// Step 2: Exchange code for tokens
async function handleCallback(code: string) {
const response = await fetch('https://auth.provider.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/callback',
client_id: process.env.OAUTH_CLIENT_ID!,
client_secret: process.env.OAUTH_CLIENT_SECRET!,
code_verifier: getStoredCodeVerifier(), // PKCE
}),
});
const { access_token, refresh_token, id_token } = await response.json();
// Store tokens securely
await storeTokens({ access_token, refresh_token, id_token });
}
##JWT (JSON Web Tokens)
Anatomy of a JWT
// JWT structure: header.payload.signature
interface JWTHeader {
alg: 'HS256' | 'RS256' | 'ES256';
typ: 'JWT';
kid?: string; // Key ID
}
interface JWTPayload {
// Registered claims
iss: string; // Issuer
sub: string; // Subject (user ID)
aud: string | string[]; // Audience
exp: number; // Expiration time (Unix timestamp)
nbf?: number; // Not before
iat: number; // Issued at
jti?: string; // JWT ID (unique identifier)
// Custom claims
email?: string;
roles?: string[];
permissions?: string[];
}
// Creating a JWT (Server-side)
import jwt from 'jsonwebtoken';
function createAccessToken(userId: string): string {
const payload: JWTPayload = {
iss: 'https://api.yourapp.com',
sub: userId,
aud: 'https://yourapp.com',
exp: Math.floor(Date.now() / 1000) + (15 * 60), // 15 minutes
iat: Math.floor(Date.now() / 1000),
email: user.email,
roles: user.roles,
};
return jwt.sign(payload, process.env.JWT_PRIVATE_KEY!, {
algorithm: 'RS256',
});
}
// Verifying a JWT (Server-side)
function verifyAccessToken(token: string): JWTPayload {
try {
return jwt.verify(token, process.env.JWT_PUBLIC_KEY!, {
algorithms: ['RS256'],
issuer: 'https://api.yourapp.com',
audience: 'https://yourapp.com',
}) as JWTPayload;
} catch (error) {
throw new UnauthorizedError('Invalid token');
}
}
JWT Best Practices
// ✅ Use short expiration times
const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days
// ✅ Implement token refresh
async function refreshAccessToken(refreshToken: string) {
const payload = verifyRefreshToken(refreshToken);
// Check if refresh token is revoked
if (await isTokenRevoked(refreshToken)) {
throw new UnauthorizedError('Token revoked');
}
return createAccessToken(payload.sub);
}
// ✅ Store tokens securely
// Server-side: Use httpOnly cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
// Client-side: Never in localStorage, use memory or httpOnly cookies
class TokenManager {
private accessToken: string | null = null;
setToken(token: string) {
this.accessToken = token; // Memory only, lost on refresh
}
getToken(): string | null {
return this.accessToken;
}
clearToken() {
this.accessToken = null;
}
}
Zero Trust Architecture
Zero Trust operates on the principle: "Never trust, always verify."
Core Principles
Implementation
// Middleware for Zero Trust
export async function zeroTrustMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
try {
// 1. Verify identity
const token = extractToken(req);
const payload = await verifyToken(token);
// 2. Verify device/context
await verifyDeviceFingerprint(req);
await verifyGeolocation(req, payload.sub);
// 3. Check for suspicious activity
await checkRateLimits(req, payload.sub);
await detectAnomalies(req, payload.sub);
// 4. Verify access level
const hasAccess = await checkPermissions(
payload.sub,
req.method,
req.path
);
if (!hasAccess) {
throw new ForbiddenError('Insufficient permissions');
}
// 5. Log access attempt
await auditLog({
userId: payload.sub,
action: `${req.method} ${req.path}`,
ipAddress: req.ip,
userAgent: req.get('user-agent'),
timestamp: new Date(),
});
req.user = payload;
next();
} catch (error) {
next(error);
}
}
Role-Based Access Control (RBAC)
// Define roles and permissions
enum Role {
ADMIN = 'admin',
EDITOR = 'editor',
VIEWER = 'viewer',
}
enum Permission {
READ_POSTS = 'read:posts',
CREATE_POSTS = 'create:posts',
UPDATE_POSTS = 'update:posts',
DELETE_POSTS = 'delete:posts',
MANAGE_USERS = 'manage:users',
}
const rolePermissions: Record<Role, Permission[]> = {
[Role.ADMIN]: [
Permission.READ_POSTS,
Permission.CREATE_POSTS,
Permission.UPDATE_POSTS,
Permission.DELETE_POSTS,
Permission.MANAGE_USERS,
],
[Role.EDITOR]: [
Permission.READ_POSTS,
Permission.CREATE_POSTS,
Permission.UPDATE_POSTS,
],
[Role.VIEWER]: [Permission.READ_POSTS],
};
// Check permissions
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.user.roles;
const hasPermission = userRoles.some((role: Role) =>
rolePermissions[role]?.includes(permission)
);
if (!hasPermission) {
throw new ForbiddenError(`Requires permission: ${permission}`);
}
next();
};
}
// Usage
app.delete('/api/posts/:id',
zeroTrustMiddleware,
requirePermission(Permission.DELETE_POSTS),
deletePost
);
Security Best Practices
1. Secure Password Handling
import bcrypt from 'bcrypt';
// Hash password
async function hashPassword(password: string): Promise<string> {
const SALT_ROUNDS = 12;
return bcrypt.hash(password, SALT_ROUNDS);
}
// Verify password
async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Password strength validation
function validatePassword(password: string): boolean {
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return (
password.length >= minLength &&
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasSpecialChar
);
}
2. CSRF Protection
import { randomBytes } from 'crypto';
// Generate CSRF token
function generateCSRFToken(): string {
return randomBytes(32).toString('hex');
}
// CSRF middleware
function csrfProtection(req: Request, res: Response, next: NextFunction) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const token = req.headers['x-csrf-token'] || req.body._csrf;
const sessionToken = req.session.csrfToken;
if (!token || token !== sessionToken) {
throw new ForbiddenError('Invalid CSRF token');
}
next();
}
// Set CSRF token
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
3. Rate Limiting
import rateLimit from 'express-rate-limit';
// Basic rate limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// Stricter limits for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // Don't count successful auth attempts
});
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);
// Advanced: Per-user rate limiting
import Redis from 'ioredis';
const redis = new Redis();
async function checkUserRateLimit(userId: string, limit: number): Promise<boolean> {
const key = `rate_limit:${userId}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, 3600); // 1 hour window
}
return current <= limit;
}
4. SQL Injection Prevention
// ❌ NEVER do this
function getUser(userId: string) {
return db.query(`SELECT * FROM users WHERE id = ${userId}`);
}
// ✅ Always use parameterized queries
function getUser(userId: string) {
return db.query('SELECT * FROM users WHERE id = ?', [userId]);
}
// ✅ With Prisma ORM
async function getUser(userId: string) {
return prisma.user.findUnique({
where: { id: userId }
});
}
5. XSS Prevention
import DOMPurify from 'isomorphic-dompurify';
// Sanitize user input
function sanitizeHTML(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
}
// Content Security Policy
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
);
next();
});
Multi-Factor Authentication (MFA)
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
// Generate MFA secret
async function setupMFA(userId: string) {
const secret = speakeasy.generateSecret({
name: `YourApp (${userId})`,
length: 32,
});
// Store secret in database
await db.user.update({
where: { id: userId },
data: { mfaSecret: secret.base32 }
});
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url!);
return {
secret: secret.base32,
qrCode,
};
}
// Verify MFA token
async function verifyMFA(userId: string, token: string): Promise<boolean> {
const user = await db.user.findUnique({
where: { id: userId },
select: { mfaSecret: true }
});
if (!user?.mfaSecret) {
throw new Error('MFA not set up');
}
return speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token,
window: 1, // Allow 1 step before/after
});
}
Conclusion
Implementing robust authentication and authorization is crucial for modern web applications. Combine OAuth 2.0 for delegated authorization, JWTs for stateless authentication, and Zero Trust principles for comprehensive security.