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