Files
flyer-crawler.projectium.com/src/middleware/tsoaAuthentication.ts
Torben Sorensen 2d2cd52011
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
Massive Dependency Modernization Project
2026-02-13 00:34:22 -08:00

197 lines
6.4 KiB
TypeScript

// src/middleware/tsoaAuthentication.ts
/**
* @file tsoa Authentication Middleware
*
* Provides JWT authentication for tsoa-generated routes.
* This middleware bridges tsoa's @Security decorators with the existing
* Passport JWT strategy used throughout the application.
*
* tsoa calls this function when a controller method is decorated with:
* - @Security('bearerAuth') - requires JWT authentication
*
* The security names must match those defined in tsoa.json:
* - bearerAuth: type "http", scheme "bearer", bearerFormat "JWT"
*
* @see tsoa.json for security definitions
* @see src/config/passport.ts for JWT strategy implementation
*/
import { Request } from 'express';
import * as jwt from 'jsonwebtoken';
import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
import type { UserProfile } from '../types';
/**
* Custom error class for authentication failures.
* tsoa catches errors thrown from expressAuthentication and converts them
* to appropriate HTTP responses.
*/
class AuthenticationError extends Error {
public status: number;
constructor(message: string, status: number = 401) {
super(message);
this.name = 'AuthenticationError';
this.status = status;
}
}
/**
* JWT payload structure matching the tokens issued by the application.
* This interface defines what we expect to find in a decoded JWT.
*/
interface JwtPayload {
user_id: string;
email?: string;
role?: string;
iat?: number;
exp?: number;
}
/**
* tsoa Authentication Handler.
*
* This function is called by tsoa-generated routes when a controller method
* is decorated with @Security('bearerAuth'). It validates the JWT token
* and retrieves the user profile from the database.
*
* @param request - Express request object
* @param securityName - Name of the security scheme (must match tsoa.json)
* @param scopes - Optional array of required scopes (not currently used)
* @returns Promise resolving to the authenticated UserProfile
* @throws AuthenticationError when authentication fails
*
* @example
* // In a tsoa controller:
* @Security('bearerAuth')
* @Get('profile')
* public async getProfile(@Request() req: Express.Request): Promise<UserProfile> {
* // req.user is populated by expressAuthentication
* return req.user as UserProfile;
* }
*/
export async function expressAuthentication(
request: Request,
securityName: string,
_scopes?: string[],
): Promise<UserProfile> {
const requestLog = request.log || logger;
// Validate security scheme name
if (securityName !== 'bearerAuth') {
requestLog.error({ securityName }, '[tsoa Auth] Unknown security scheme requested');
throw new AuthenticationError(`Unknown security scheme: ${securityName}`);
}
// Extract JWT token from Authorization header
const authHeader = request.headers.authorization;
if (!authHeader) {
requestLog.debug('[tsoa Auth] No Authorization header present');
throw new AuthenticationError('No authorization header provided');
}
// Validate Bearer token format
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
requestLog.debug(
{ authHeader: authHeader.substring(0, 20) + '...' },
'[tsoa Auth] Invalid Authorization header format',
);
throw new AuthenticationError('Invalid authorization header format. Expected: Bearer <token>');
}
const token = parts[1];
// Validate JWT_SECRET is configured
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
requestLog.error('[tsoa Auth] JWT_SECRET is not configured');
throw new AuthenticationError('Server configuration error', 500);
}
try {
// Verify and decode the JWT
const decoded = jwt.verify(token, jwtSecret) as JwtPayload;
requestLog.debug({ user_id: decoded.user_id }, '[tsoa Auth] JWT token verified successfully');
// Validate required claims
if (!decoded.user_id) {
requestLog.warn('[tsoa Auth] JWT payload missing user_id claim');
throw new AuthenticationError('Invalid token: missing user_id');
}
// Fetch user profile from database to ensure user is still valid
const userProfile = await db.userRepo.findUserProfileById(decoded.user_id, requestLog);
if (!userProfile) {
requestLog.warn({ user_id: decoded.user_id }, '[tsoa Auth] User not found in database');
throw new AuthenticationError('User not found');
}
requestLog.debug(
{ user_id: decoded.user_id, role: userProfile.role },
'[tsoa Auth] User authenticated successfully',
);
// Attach user to request for use in controller methods
// Note: tsoa also passes the resolved value to the controller
request.user = userProfile;
return userProfile;
} catch (error) {
// Handle specific JWT errors
if (error instanceof jwt.TokenExpiredError) {
requestLog.debug('[tsoa Auth] JWT token expired');
throw new AuthenticationError('Token expired');
}
if (error instanceof jwt.JsonWebTokenError) {
requestLog.debug({ error: error.message }, '[tsoa Auth] JWT verification failed');
throw new AuthenticationError('Invalid token');
}
// Re-throw AuthenticationError as-is
if (error instanceof AuthenticationError) {
throw error;
}
// Log and wrap unexpected errors
requestLog.error({ error }, '[tsoa Auth] Unexpected error during authentication');
throw new AuthenticationError('Authentication failed');
}
}
/**
* Type guard to check if a user object is a valid UserProfile.
* Useful for runtime validation of req.user in controllers.
*
* @param user - Object to validate
* @returns True if the object is a valid UserProfile
*/
export function isUserProfile(user: unknown): user is UserProfile {
return (
typeof user === 'object' &&
user !== null &&
'role' in user &&
'user' in user &&
typeof (user as { user: unknown }).user === 'object' &&
(user as { user: unknown }).user !== null &&
'user_id' in ((user as { user: unknown }).user as object)
);
}
/**
* Validates that the authenticated user has admin role.
* For use in controller methods that require admin access.
*
* @param user - UserProfile from authentication
* @throws AuthenticationError with 403 status if user is not an admin
*/
export function requireAdminRole(user: UserProfile): void {
if (user.role !== 'admin') {
throw new AuthenticationError('Forbidden: Administrator access required', 403);
}
}