@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
307 lines • 11.1 kB
JavaScript
;
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