xpeditis2.0/apps/backend/src/infrastructure/security/security.config.ts
2026-06-23 10:04:12 +02:00

217 lines
5.6 KiB
TypeScript

/**
* Security Configuration
*
* Implements OWASP Top 10 security best practices
*/
import { HelmetOptions } from 'helmet';
export const helmetConfig: HelmetOptions = {
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Required for inline styles in some frameworks
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
// Cross-Origin Embedder Policy
crossOriginEmbedderPolicy: false, // Set to true in production if needed
// Cross-Origin Opener Policy
crossOriginOpenerPolicy: { policy: 'same-origin' },
// Cross-Origin Resource Policy
crossOriginResourcePolicy: { policy: 'same-origin' },
// DNS Prefetch Control
dnsPrefetchControl: { allow: false },
// Frameguard
frameguard: { action: 'deny' },
// Hide Powered-By
hidePoweredBy: true,
// HSTS (HTTP Strict Transport Security)
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
// IE No Open
ieNoOpen: true,
// No Sniff
noSniff: true,
// Origin Agent Cluster
originAgentCluster: true,
// Permitted Cross-Domain Policies
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
// Referrer Policy
referrerPolicy: { policy: 'no-referrer' },
// XSS Filter
xssFilter: true,
};
/**
* Rate Limiting Configuration
*/
export const rateLimitConfig = {
// Global rate limit
global: {
ttl: 60, // 60 seconds
limit: 100, // 100 requests per minute
},
// Auth endpoints (more strict)
auth: {
ttl: 60,
limit: 5, // 5 login attempts per minute
},
// Search endpoints
search: {
ttl: 60,
limit: 30, // 30 searches per minute
},
// Booking endpoints
booking: {
ttl: 60,
limit: 20, // 20 bookings per minute
},
};
/**
* Allowed CORS origins.
*
* Reads CORS_ORIGIN (comma-separated list — supports several frontend hosts such
* as app.* and www.*), falling back to FRONTEND_URL, then to localhost in dev.
* With `credentials: true` the browser requires an exact origin echo (no `*`),
* so we resolve the request origin against this allow-list per request.
*/
const allowedOrigins: string[] = (
process.env.CORS_ORIGIN ||
process.env.FRONTEND_URL ||
'http://localhost:3000,http://localhost:3001'
)
.split(',')
.map(origin => origin.trim())
.filter(Boolean);
/**
* CORS Configuration
*/
export const corsConfig = {
origin: (
requestOrigin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void
): void => {
// Allow non-browser clients (no Origin header: server-to-server, curl, health checks).
if (!requestOrigin || allowedOrigins.includes(requestOrigin)) {
callback(null, true);
return;
}
callback(null, false);
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token'],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
maxAge: 86400, // 24 hours
};
/**
* Password Policy
*/
export const passwordPolicy = {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true,
maxLength: 128,
preventCommon: true, // Prevent common passwords
preventReuse: 5, // Last 5 passwords
};
/**
* File Upload Configuration
*/
export const fileUploadConfig = {
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedMimeTypes: [
'application/pdf',
'image/jpeg',
'image/png',
'image/webp',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
],
allowedExtensions: ['.pdf', '.jpg', '.jpeg', '.png', '.webp', '.xls', '.xlsx', '.csv'],
scanForViruses: process.env.NODE_ENV === 'production',
};
/**
* Auth Cookie Configuration
*
* Tokens are delivered as httpOnly cookies so they are not readable from
* JavaScript (XSS mitigation). COOKIE_DOMAIN must be set in production when
* the API and the frontend live on different subdomains (e.g. ".xpeditis.com").
*/
export const AUTH_COOKIE_NAMES = {
accessToken: 'accessToken',
refreshToken: 'refreshToken',
/** Non-httpOnly flag the frontend reads to know a session exists (contains no token) */
session: 'xpeditis_session',
} as const;
export function authCookieOptions(options?: { maxAgeMs?: number; httpOnly?: boolean }): {
httpOnly: boolean;
secure: boolean;
sameSite: 'lax' | 'strict' | 'none';
path: string;
domain?: string;
maxAge?: number;
} {
// SameSite must be 'none' when the frontend and the API live on different
// sites (cross-origin), otherwise the browser drops the auth cookies set in
// the cross-site login XHR response. 'none' REQUIRES Secure (HTTPS).
// Configurable via COOKIE_SAMESITE; defaults to 'lax' for same-site setups.
const sameSite = (process.env.COOKIE_SAMESITE?.toLowerCase() as 'lax' | 'strict' | 'none') || 'lax';
const secure = process.env.NODE_ENV === 'production' || sameSite === 'none';
return {
httpOnly: options?.httpOnly ?? true,
secure,
sameSite,
path: '/',
...(process.env.COOKIE_DOMAIN ? { domain: process.env.COOKIE_DOMAIN } : {}),
...(options?.maxAgeMs ? { maxAge: options.maxAgeMs } : {}),
};
}
/**
* Brute Force Protection
*/
export const bruteForceConfig = {
freeRetries: 3,
minWait: 5 * 60 * 1000, // 5 minutes
maxWait: 60 * 60 * 1000, // 1 hour
lifetime: 24 * 60 * 60, // 24 hours
};