import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { Request, Response, NextFunction } from 'express';
// ============================================
// Configuration
// ============================================
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-key-change-in-production';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
const BCRYPT_ROUNDS = 12;
// ============================================
// Types
// ============================================
export interface TokenPayload {
userId: string;
email: string;
role: 'user' | 'admin';
}
export interface AuthRequest extends Request {
user?: TokenPayload;
}
// ============================================
// Password Hashing
// ============================================
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, BCRYPT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// ============================================
// JWT Token Functions
// ============================================
export function generateToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
export function verifyToken(token: string): TokenPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
} catch {
return null;
}
}
export function decodeToken(token: string): TokenPayload | null {
try {
return jwt.decode(token) as TokenPayload;
} catch {
return null;
}
}
// ============================================
// Middleware
// ============================================
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
const payload = verifyToken(token);
if (!payload) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
req.user = payload;
next();
}
export function requireRole(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
export function optionalAuth(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
const payload = verifyToken(token);
if (payload) {
req.user = payload;
}
}
next();
}
// ============================================
// Refresh Token (In-Memory - Use Redis in production)
// ============================================
const refreshTokens = new Map<string, { userId: string; expiresAt: Date }>();
export function generateRefreshToken(userId: string): string {
const token = Buffer.from(`${userId}-${Date.now()}-${Math.random()}`).toString('base64');
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
refreshTokens.set(token, { userId, expiresAt });
return token;
}
export function validateRefreshToken(token: string): string | null {
const data = refreshTokens.get(token);
if (!data) return null;
if (data.expiresAt < new Date()) {
refreshTokens.delete(token);
return null;
}
return data.userId;
}
export function revokeRefreshToken(token: string): void {
refreshTokens.delete(token);
}
// ============================================
// Rate Limiting (Simple In-Memory)
// ============================================
const requestCounts = new Map<string, { count: number; resetAt: Date }>();
export function rateLimiter(maxRequests: number, windowMs: number) {
return (req: Request, res: Response, next: NextFunction) => {
const ip = req.ip || req.socket.remoteAddress || 'unknown';
const now = new Date();
const record = requestCounts.get(ip);
if (!record || record.resetAt < now) {
requestCounts.set(ip, { count: 1, resetAt: new Date(now.getTime() + windowMs) });
return next();
}
if (record.count >= maxRequests) {
const retryAfter = Math.ceil((record.resetAt.getTime() - now.getTime()) / 1000);
res.set('Retry-After', retryAfter.toString());
return res.status(429).json({
error: 'Too many requests',
retryAfter,
});
}
record.count++;
next();
};
}
// ============================================
// CSRF Protection
// ============================================
const csrfTokens = new Map<string, Date>();
export function generateCsrfToken(): string {
const token = Buffer.from(`${Date.now()}-${Math.random()}`).toString('base64');
csrfTokens.set(token, new Date(Date.now() + 3600000)); // 1 hour
return token;
}
export function csrfProtection(req: Request, res: Response, next: NextFunction) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const token = req.headers['x-csrf-token'] as string;
if (!token || !csrfTokens.has(token)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
const expiry = csrfTokens.get(token)!;
if (expiry < new Date()) {
csrfTokens.delete(token);
return res.status(403).json({ error: 'CSRF token expired' });
}
next();
}
// ============================================
// Security Headers Middleware
// ============================================
export function securityHeaders(req: Request, res: Response, next: NextFunction) {
res.set({
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Content-Security-Policy': "default-src 'self'",
});
next();
}