Authentication Patterns
Overview
Authentication and authorization patterns for securing backend applications.
Authentication Methods
JWT (JSON Web Tokens)
┌─────────────────────────────────────────────────────────┐
│ Header.Payload.Signature │
│ │
│ Header: { "alg": "HS256", "typ": "JWT" } │
│ Payload: { "sub": "user123", "exp": 1609459200, ... } │
│ Signature: HMACSHA256(base64(header) + "." + │
│ base64(payload), secret) │
└─────────────────────────────────────────────────────────┘
Token Structure:
interface JWTPayload {
sub: string; // Subject (user ID)
iat: number; // Issued at
exp: number; // Expiration
iss?: string; // Issuer
aud?: string; // Audience
roles?: string[]; // Custom claims
}
Implementation:
import jwt from 'jsonwebtoken';
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ sub: user.id, roles: user.roles },
process.env.JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
return { accessToken, refreshToken };
}
function verifyAccessToken(token: string): JWTPayload {
return jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
}
Session-Based Authentication
// Session storage (Redis recommended for production)
interface Session {
userId: string;
createdAt: Date;
expiresAt: Date;
userAgent?: string;
ipAddress?: string;
}
// Login
async function login(email: string, password: string, req: Request) {
const user = await findUserByEmail(email);
if (!user || !await verifyPassword(password, user.passwordHash)) {
throw new AuthError('Invalid credentials');
}
const sessionId = generateSecureId();
await redis.set(`session:${sessionId}`, JSON.stringify({
userId: user.id,
createdAt: new Date(),
expiresAt: addDays(new Date(), 7),
userAgent: req.headers['user-agent'],
}), 'EX', 7 * 24 * 60 * 60);
return sessionId;
}
// Middleware
async function authenticate(req: Request, res: Response, next: NextFunction) {
const sessionId = req.cookies.session;
if (!sessionId) return res.status(401).json({ error: 'Unauthorized' });
const session = await redis.get(`session:${sessionId}`);
if (!session) return res.status(401).json({ error: 'Session expired' });
req.user = JSON.parse(session);
next();
}
OAuth 2.0 / OpenID Connect
┌──────────┐ ┌──────────────┐
│ Client │──────1. Auth Request──────▶│ Auth │
│ (App) │◀─────2. Auth Code──────────│ Provider │
│ │──────3. Exchange Code──────▶│ (Google, │
│ │◀─────4. Access Token───────│ GitHub) │
│ │──────5. API Requests───────▶│ │
└──────────┘ └──────────────┘
Implementation with Passport.js:
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
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) => {
const user = await findOrCreateUser({
provider: 'google',
providerId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
});
done(null, user);
}
));
// Routes
app.get('/auth/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => res.redirect('/dashboard')
);
Password Security
Hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
Password Requirements
const PASSWORD_RULES = {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecial: true,
};
function validatePassword(password: string): string[] {
const errors: string[] = [];
if (password.length < PASSWORD_RULES.minLength) {
errors.push(`Password must be at least ${PASSWORD_RULES.minLength} characters`);
}
if (PASSWORD_RULES.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('Password must contain an uppercase letter');
}
if (PASSWORD_RULES.requireLowercase && !/[a-z]/.test(password)) {
errors.push('Password must contain a lowercase letter');
}
if (PASSWORD_RULES.requireNumber && !/\d/.test(password)) {
errors.push('Password must contain a number');
}
if (PASSWORD_RULES.requireSpecial && !/[!@#$%^&*]/.test(password)) {
errors.push('Password must contain a special character');
}
return errors;
}
Authorization Patterns
Role-Based Access Control (RBAC)
type Role = 'admin' | 'editor' | 'viewer';
const PERMISSIONS: Record<Role, string[]> = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
};
function hasPermission(user: User, permission: string): boolean {
return user.roles.some(role =>
PERMISSIONS[role]?.includes(permission)
);
}
// Middleware
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!hasPermission(req.user, permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Usage
app.delete('/users/:id', requirePermission('manage_users'), deleteUser);
Attribute-Based Access Control (ABAC)
interface Policy {
resource: string;
action: string;
condition: (user: User, resource: any) => boolean;
}
const policies: Policy[] = [
{
resource: 'document',
action: 'edit',
condition: (user, doc) =>
doc.ownerId === user.id || user.roles.includes('admin'),
},
{
resource: 'document',
action: 'delete',
condition: (user, doc) =>
doc.ownerId === user.id,
},
];
function canPerform(user: User, action: string, resource: string, resourceData: any): boolean {
const policy = policies.find(p =>
p.resource === resource && p.action === action
);
if (!policy) return false;
return policy.condition(user, resourceData);
}
Security Best Practices
Token Storage
| Storage | Access Token | Refresh Token | |---------|--------------|---------------| | Memory | Yes | No | | HttpOnly Cookie | Yes (CSRF protection needed) | Yes | | localStorage | Avoid | Never | | sessionStorage | Last resort | Never |
Rate Limiting
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: { error: 'Too many login attempts, try again later' },
standardHeaders: true,
});
app.post('/auth/login', authLimiter, loginHandler);
Account Lockout
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes
async function handleLogin(email: string, password: string) {
const user = await findUserByEmail(email);
if (user.lockedUntil && user.lockedUntil > new Date()) {
throw new AuthError('Account locked. Try again later.');
}
if (!await verifyPassword(password, user.passwordHash)) {
await incrementFailedAttempts(user.id);
if (user.failedAttempts + 1 >= MAX_FAILED_ATTEMPTS) {
await lockAccount(user.id, LOCKOUT_DURATION);
}
throw new AuthError('Invalid credentials');
}
await resetFailedAttempts(user.id);
return generateTokens(user);
}
Secure Headers
import helmet from 'helmet';
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
}));
Multi-Factor Authentication
TOTP (Time-based One-Time Password)
import speakeasy from 'speakeasy';
import qrcode from 'qrcode';
// Setup MFA
async function setupMFA(userId: string) {
const secret = speakeasy.generateSecret({
name: `MyApp (${user.email})`,
});
await saveUserMFASecret(userId, secret.base32);
const qrCodeUrl = await qrcode.toDataURL(secret.otpauth_url);
return { secret: secret.base32, qrCode: qrCodeUrl };
}
// Verify MFA code
function verifyMFACode(secret: string, code: string): boolean {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token: code,
window: 1, // Allow 1 step tolerance
});
}
Refresh Token Rotation
async function refreshTokens(refreshToken: string) {
const payload = verifyRefreshToken(refreshToken);
// Check if token is in blocklist (revoked)
if (await isTokenRevoked(refreshToken)) {
throw new AuthError('Token revoked');
}
// Revoke old refresh token
await revokeToken(refreshToken);
// Generate new tokens
const user = await findUserById(payload.sub);
return generateTokens(user);
}
Authentication and authorization patterns for secure applications