UNPKG

@noony-serverless/core

Version:

A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript

307 lines 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createSecurityMiddleware = exports.SecurityMiddleware = void 0; const core_1 = require("../core"); /** * Consolidated SecurityMiddleware that combines authentication, security headers, and audit logging. * * This middleware replaces the need for separate: * - AuthenticationMiddleware * - SecurityHeadersMiddleware * - SecurityAuditMiddleware * * @example * Basic security with authentication and headers: * ```typescript * const handler = new Handler() * .use(new SecurityMiddleware({ * authentication: { * tokenVerifier: { * verifyToken: async (token) => jwt.verify(token, secret) * }, * extractToken: (req) => req.headers.authorization?.replace('Bearer ', '') * }, * headers: { * contentSecurityPolicy: "default-src 'self'", * xFrameOptions: 'DENY', * strictTransportSecurity: 'max-age=31536000; includeSubDomains' * }, * audit: { * logFailedAuth: true, * trackSuspiciousIPs: true * } * })) * .handle(async (context) => { * // context.user is populated if authentication succeeds * const user = context.user; * return { message: `Hello ${user?.name}` }; * }); * ``` * * @example * Advanced security with custom auditing: * ```typescript * const handler = new Handler() * .use(new SecurityMiddleware({ * authentication: { * tokenVerifier: customTokenVerifier, * skipPaths: ['/health', '/metrics'], * onAuthFailure: async (error, context) => { * await logSecurityEvent('auth_failure', { * ip: context.req.ip, * error: error.message * }); * } * }, * audit: { * customAuditor: async (event, context) => { * await sendToSecuritySystem(event); * }, * alertThresholds: { * failedAttempts: 5, * timeWindowMs: 300000 // 5 minutes * } * } * })); * ``` */ class SecurityMiddleware { config; failedAttempts = new Map(); constructor(config = {}) { this.config = { authentication: {}, headers: { xFrameOptions: 'DENY', xContentTypeOptions: 'nosniff', xXssProtection: '1; mode=block', referrerPolicy: 'strict-origin-when-cross-origin', ...config.headers, }, audit: { logFailedAuth: true, trackSuspiciousIPs: false, enableMetrics: true, ...config.audit, }, ...config, }; } async before(context) { // 1. Set security headers first (always apply) await this.setSecurityHeaders(context); // 2. Perform authentication if configured if (this.config.authentication?.tokenVerifier) { await this.authenticateRequest(context); } } async after(context) { // Audit successful operations if enabled if (this.config.audit?.logSuccessfulAuth && context.user) { await this.auditSecurityEvent({ type: 'AUTH_SUCCESS', ip: context.req.ip, userAgent: context.req.userAgent, path: context.req.path, timestamp: new Date(), details: { userId: context.user?.id }, }, context); } } async onError(error, context) { // Audit authentication failures if (error instanceof core_1.AuthenticationError && this.config.audit?.logFailedAuth) { const ip = context.req.ip || 'unknown'; // Track failed attempts for suspicious IP detection if (this.config.audit?.trackSuspiciousIPs) { await this.trackFailedAttempt(ip, context); } await this.auditSecurityEvent({ type: 'AUTH_FAILURE', ip, userAgent: context.req.userAgent, path: context.req.path, timestamp: new Date(), details: { error: error.message }, }, context); // Custom auth failure handler if (this.config.authentication?.onAuthFailure) { await this.config.authentication.onAuthFailure(error, context); } } } async setSecurityHeaders(context) { const headers = this.config.headers; if (headers.contentSecurityPolicy) { context.res.header('Content-Security-Policy', headers.contentSecurityPolicy); } if (headers.xFrameOptions) { context.res.header('X-Frame-Options', headers.xFrameOptions); } if (headers.strictTransportSecurity) { context.res.header('Strict-Transport-Security', headers.strictTransportSecurity); } if (headers.xContentTypeOptions) { context.res.header('X-Content-Type-Options', headers.xContentTypeOptions); } if (headers.xXssProtection) { context.res.header('X-XSS-Protection', headers.xXssProtection); } if (headers.referrerPolicy) { context.res.header('Referrer-Policy', headers.referrerPolicy); } if (headers.permissionsPolicy) { context.res.header('Permissions-Policy', headers.permissionsPolicy); } // Apply custom headers if (headers.customHeaders) { Object.entries(headers.customHeaders).forEach(([name, value]) => { context.res.header(name, value); }); } } async authenticateRequest(context) { const authConfig = this.config.authentication; // Skip authentication for specified paths if (authConfig.skipPaths?.some((path) => context.req.path?.startsWith(path))) { return; } // Extract token const token = authConfig.extractToken ? authConfig.extractToken(context.req) : this.extractTokenFromHeader(context.req); if (!token) { if (!authConfig.optional) { throw new core_1.AuthenticationError('No authentication token provided'); } return; } try { // Verify token using the provided verifier const user = await authConfig.tokenVerifier.verifyToken(token); context.user = user; } catch (error) { throw new core_1.AuthenticationError('Invalid authentication token'); } } extractTokenFromHeader(req) { const authHeader = req.headers?.authorization || req.headers?.Authorization; if (typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } return null; } async trackFailedAttempt(ip, context) { const now = Date.now(); const threshold = this.config.audit?.alertThresholds?.failedAttempts || 5; const timeWindow = this.config.audit?.alertThresholds?.timeWindowMs || 300000; // 5 minutes const existing = this.failedAttempts.get(ip); if (!existing) { this.failedAttempts.set(ip, { count: 1, firstAttempt: now }); return; } // Reset if outside time window if (now - existing.firstAttempt > timeWindow) { this.failedAttempts.set(ip, { count: 1, firstAttempt: now }); return; } // Increment count existing.count++; // Check if threshold exceeded if (existing.count >= threshold) { await this.auditSecurityEvent({ type: 'THRESHOLD_EXCEEDED', ip, userAgent: context.req.userAgent, path: context.req.path, timestamp: new Date(), details: { failedAttempts: existing.count, timeWindowMs: timeWindow, firstAttemptTime: new Date(existing.firstAttempt), }, }, context); // Optionally reset counter or keep tracking this.failedAttempts.delete(ip); } } async auditSecurityEvent(event, context) { // Use custom auditor if provided if (this.config.audit?.customAuditor) { await this.config.audit.customAuditor(event, context); return; } // Default logging const logLevel = event.type.includes('FAILURE') || event.type.includes('EXCEEDED') ? 'warn' : 'info'; console[logLevel]('[SecurityMiddleware]', { type: event.type, ip: event.ip, path: event.path, timestamp: event.timestamp.toISOString(), userAgent: event.userAgent, details: event.details, }); // Store in business data for downstream processing if (!context.businessData.has('securityEvents')) { context.businessData.set('securityEvents', []); } context.businessData.get('securityEvents').push(event); } } exports.SecurityMiddleware = SecurityMiddleware; /** * Factory function for creating SecurityMiddleware with common configurations */ exports.createSecurityMiddleware = { /** * Basic security setup with common headers and JWT authentication */ basic: (tokenVerifier) => new SecurityMiddleware({ authentication: { tokenVerifier }, headers: { contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY', strictTransportSecurity: 'max-age=31536000', }, audit: { logFailedAuth: true }, }), /** * Advanced security with audit tracking and suspicious IP monitoring */ advanced: (tokenVerifier) => new SecurityMiddleware({ authentication: { tokenVerifier }, headers: { contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'", xFrameOptions: 'DENY', strictTransportSecurity: 'max-age=31536000; includeSubDomains', referrerPolicy: 'strict-origin-when-cross-origin', permissionsPolicy: 'geolocation=(), microphone=(), camera=()', }, audit: { logFailedAuth: true, logSuccessfulAuth: true, trackSuspiciousIPs: true, alertThresholds: { failedAttempts: 5, timeWindowMs: 300000, }, }, }), /** * Headers only - no authentication */ headersOnly: () => new SecurityMiddleware({ headers: { contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY', xContentTypeOptions: 'nosniff', xXssProtection: '1; mode=block', referrerPolicy: 'strict-origin-when-cross-origin', }, }), }; //# sourceMappingURL=SecurityMiddleware.js.map