UNPKG

@noony-serverless/core

Version:

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

422 lines 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyAuthTokenMiddleware = exports.AuthenticationMiddleware = void 0; const errors_1 = require("../core/errors"); const logger_1 = require("../core/logger"); // Simple in-memory store for rate limiting (use Redis in production) const rateLimitStore = new Map(); /** * Enhanced JWT validation with comprehensive security checks */ const validateJWTSecurity = (payload, options = {}, clientIP) => { const now = Math.floor(Date.now() / 1000); const clockTolerance = options.clockTolerance || 60; // Validate expiration time if (payload.exp !== undefined) { if (payload.exp <= now - clockTolerance) { logger_1.logger.warn('Token expired', { expiredAt: new Date(payload.exp * 1000).toISOString(), currentTime: new Date(now * 1000).toISOString(), clientIP, }); throw new errors_1.AuthenticationError('Token expired'); } } // Validate not-before time if (payload.nbf !== undefined) { if (payload.nbf > now + clockTolerance) { logger_1.logger.warn('Token used before valid time', { notBefore: new Date(payload.nbf * 1000).toISOString(), currentTime: new Date(now * 1000).toISOString(), clientIP, }); throw new errors_1.AuthenticationError('Token not yet valid'); } } // Validate issued-at time if maxTokenAge is specified if (options.maxTokenAge && payload.iat !== undefined) { const tokenAge = now - payload.iat; if (tokenAge > options.maxTokenAge) { logger_1.logger.warn('Token too old', { tokenAge: `${tokenAge}s`, maxAge: `${options.maxTokenAge}s`, clientIP, }); throw new errors_1.AuthenticationError('Token too old'); } } // Validate required claims if (options.requiredClaims) { if (options.requiredClaims.issuer && payload.iss !== options.requiredClaims.issuer) { logger_1.logger.warn('Invalid token issuer', { expected: options.requiredClaims.issuer, received: payload.iss, clientIP, }); throw new errors_1.SecurityError('Invalid token issuer'); } if (options.requiredClaims.audience) { const requiredAud = options.requiredClaims.audience; const tokenAud = payload.aud; let isValidAudience = false; if (Array.isArray(requiredAud)) { isValidAudience = Array.isArray(tokenAud) ? tokenAud.some((aud) => requiredAud.includes(aud)) : requiredAud.includes(tokenAud || ''); } else { isValidAudience = Array.isArray(tokenAud) ? tokenAud.includes(requiredAud) : tokenAud === requiredAud; } if (!isValidAudience) { logger_1.logger.warn('Invalid token audience', { expected: requiredAud, received: tokenAud, clientIP, }); throw new errors_1.SecurityError('Invalid token audience'); } } } }; /** * Check rate limiting for authentication attempts */ const checkRateLimit = (identifier, options) => { if (!options.rateLimiting) return; const now = Date.now(); const key = `auth:${identifier}`; const limit = rateLimitStore.get(key); if (limit && now < limit.resetTime) { if (limit.attempts >= options.rateLimiting.maxAttempts) { logger_1.logger.warn('Rate limit exceeded for authentication', { identifier, attempts: limit.attempts, resetTime: new Date(limit.resetTime).toISOString(), }); throw new errors_1.SecurityError('Too many authentication attempts. Please try again later.'); } limit.attempts++; } else { rateLimitStore.set(key, { attempts: 1, resetTime: now + options.rateLimiting.windowMs, }); } }; async function verifyToken(tokenVerificationPort, context, options = {}) { const authHeader = context.req.headers?.authorization; const clientIP = context.req.ip || context.req.headers?.['x-forwarded-for'] || 'unknown'; const userAgent = context.req.headers?.['user-agent']; if (!authHeader) { logger_1.logger.warn('Missing authorization header', { clientIP, userAgent }); throw new errors_1.HttpError(401, 'No authorization header'); } const authHeaderString = Array.isArray(authHeader) ? authHeader[0] : authHeader; const token = authHeaderString?.split('Bearer ')[1]; if (!token) { logger_1.logger.warn('Invalid token format', { clientIP, userAgent }); throw new errors_1.AuthenticationError('Invalid token format'); } // Check rate limiting checkRateLimit(clientIP, options); try { // Verify token through port const user = await tokenVerificationPort.verifyToken(token); // If user has JWT payload, validate security aspects if (user && typeof user === 'object' && 'exp' in user) { validateJWTSecurity(user, options, clientIP); // Check token blacklist if configured if (options.isTokenBlacklisted && 'jti' in user) { const isBlacklisted = await options.isTokenBlacklisted(user.jti); if (isBlacklisted) { logger_1.logger.warn('Blacklisted token used', { tokenId: user.jti, clientIP, userAgent, }); throw new errors_1.SecurityError('Token has been revoked'); } } } context.user = user; logger_1.logger.debug('Successful authentication', { userId: typeof user === 'object' && user && 'sub' in user ? String(user.sub) : 'unknown', clientIP, }); } catch (error) { // Log failed authentication attempt logger_1.logger.warn('Authentication failed', { error: error instanceof Error ? error.message : 'Unknown error', clientIP, userAgent, tokenPreview: token.substring(0, 10) + '...', }); if (error instanceof errors_1.HttpError) { throw error; } throw new errors_1.AuthenticationError('Invalid authentication'); } } /** * Class-based authentication middleware with comprehensive security features. * Provides JWT validation, rate limiting, token blacklisting, and security logging. * * @template TUser - The type of user data returned by the token verification port * @template TBody - The type of the request body payload (preserves type chain) * * @example * Basic JWT authentication: * ```typescript * import { Handler, AuthenticationMiddleware } from '@noony-serverless/core'; * import jwt from 'jsonwebtoken'; * * interface User { * id: string; * email: string; * roles: string[]; * } * * class JWTVerifier implements CustomTokenVerificationPort<User> { * async verifyToken(token: string): Promise<User> { * const payload = jwt.verify(token, process.env.JWT_SECRET!) as any; * return { * id: payload.sub, * email: payload.email, * roles: payload.roles || [] * }; * } * } * * const protectedHandler = new Handler() * .use(new AuthenticationMiddleware(new JWTVerifier())) * .handle(async (request, context) => { * const user = context.user as User; * return { * success: true, * data: { message: `Hello ${user.email}`, userId: user.id } * }; * }); * ``` * * @example * Advanced authentication with security options: * ```typescript * const secureAuthMiddleware = new AuthenticationMiddleware( * new JWTVerifier(), * { * maxTokenAge: 1800, // 30 minutes * rateLimiting: { * maxAttempts: 5, * windowMs: 15 * 60 * 1000 // 15 minutes * }, * isTokenBlacklisted: async (tokenId) => { * return await redis.sismember('revoked_tokens', tokenId); * }, * requiredClaims: { * issuer: 'my-auth-server', * audience: 'my-api' * } * } * ); * * const secureHandler = new Handler() * .use(secureAuthMiddleware) * .handle(async (request, context) => { * // Only authenticated users reach here * return { success: true, data: 'Secure data' }; * }); * ``` * * @example * Google Cloud Functions integration: * ```typescript * import { http } from '@google-cloud/functions-framework'; * * const userProfileHandler = new Handler() * .use(new AuthenticationMiddleware(new JWTVerifier())) * .handle(async (request, context) => { * const user = context.user as User; * const profile = await getUserProfile(user.id); * return { success: true, data: profile }; * }); * * export const getUserProfile = http('getUserProfile', (req, res) => { * return userProfileHandler.execute(req, res); * }); * ``` */ class AuthenticationMiddleware { tokenVerificationPort; options; constructor(tokenVerificationPort, options = {}) { this.tokenVerificationPort = tokenVerificationPort; this.options = options; } async before(context) { await verifyToken(this.tokenVerificationPort, context, this.options); } } exports.AuthenticationMiddleware = AuthenticationMiddleware; /** * Factory function that creates an authentication middleware with token verification. * Provides a functional approach for authentication setup. * * @template TUser - The type of user data returned by the token verification port * @template TBody - The type of the request body payload (preserves type chain) * @param tokenVerificationPort - The token verification implementation * @param options - Authentication configuration options * @returns A BaseMiddleware object with authentication logic * * @example * Simple JWT authentication: * ```typescript * import { Handler, verifyAuthTokenMiddleware } from '@noony-serverless/core'; * * class SimpleJWTVerifier implements CustomTokenVerificationPort<{ userId: string }> { * async verifyToken(token: string): Promise<{ userId: string }> { * // Simple token verification logic * if (token === 'valid-token') { * return { userId: 'user-123' }; * } * throw new Error('Invalid token'); * } * } * * const handler = new Handler() * .use(verifyAuthTokenMiddleware(new SimpleJWTVerifier())) * .handle(async (request, context) => { * const user = context.user as { userId: string }; * return { success: true, userId: user.userId }; * }); * ``` * * @example * API key authentication with rate limiting: * ```typescript * interface APIKeyUser { * keyId: string; * permissions: string[]; * organization: string; * } * * class APIKeyVerifier implements CustomTokenVerificationPort<APIKeyUser> { * async verifyToken(token: string): Promise<APIKeyUser> { * const keyData = await this.validateAPIKey(token); * if (!keyData) { * throw new Error('Invalid API key'); * } * return keyData; * } * * private async validateAPIKey(key: string): Promise<APIKeyUser | null> { * // Database lookup or external validation * return { * keyId: 'key-123', * permissions: ['read', 'write'], * organization: 'org-456' * }; * } * } * * const apiHandler = new Handler() * .use(verifyAuthTokenMiddleware( * new APIKeyVerifier(), * { * rateLimiting: { * maxAttempts: 100, * windowMs: 60 * 1000 // 1 minute * } * } * )) * .handle(async (request, context) => { * const apiUser = context.user as APIKeyUser; * return { * success: true, * data: { organization: apiUser.organization } * }; * }); * ``` * * @example * Express-style middleware chain: * ```typescript * import { Handler, verifyAuthTokenMiddleware, errorHandler } from '@noony-serverless/core'; * * const authMiddleware = verifyAuthTokenMiddleware( * new JWTVerifier(), * { * maxTokenAge: 3600, * requiredClaims: { * issuer: 'my-app', * audience: 'api-users' * } * } * ); * * const protectedEndpoint = new Handler() * .use(authMiddleware) * .use(errorHandler()) * .handle(async (request, context) => { * // Authenticated user available in context.user * return { success: true, data: 'Protected resource' }; * }); * ``` * * @example * Multiple authentication strategies: * ```typescript * // Different handlers for different auth types * const jwtHandler = new Handler() * .use(verifyAuthTokenMiddleware(new JWTVerifier())) * .handle(jwtLogic); * * const apiKeyHandler = new Handler() * .use(verifyAuthTokenMiddleware(new APIKeyVerifier())) * .handle(apiKeyLogic); * * // Route based on authentication type * export const handleRequest = (req: any, res: any) => { * const authHeader = req.headers.authorization; * if (authHeader?.startsWith('Bearer jwt.')) { * return jwtHandler.execute(req, res); * } else if (authHeader?.startsWith('Bearer ak_')) { * return apiKeyHandler.execute(req, res); * } else { * res.status(401).json({ error: 'Authentication required' }); * } * }; * ``` */ const verifyAuthTokenMiddleware = (tokenVerificationPort, options = {}) => ({ async before(context) { await verifyToken(tokenVerificationPort, context, options); }, }); exports.verifyAuthTokenMiddleware = verifyAuthTokenMiddleware; /* // Example protected endpoint const protectedHandler = new Handler() .use(verifyAuthTokenMiddleware(customTokenVerificationPort)) .use(errorHandler()) .use(responseWrapperMiddleware<any>()) .handle(async (context: Context) => { const user = context.user; setResponseData(context, { message: 'Protected endpoint', user, }); }); */ //# sourceMappingURL=authenticationMiddleware.js.map