/** * JWT Authentication Middleware for HTTP transport mode. * * Validates Supabase JWT tokens and extracts user information. * Required for all /mcp endpoints in HTTP mode. */ import type { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; export interface AuthenticatedUser { userId: string; email: string | null; role: string; exp: number; } export interface AuthenticatedRequest extends Request { user?: AuthenticatedUser; } interface SupabaseJwtPayload { sub: string; // User ID email?: string; role?: string; aud?: string; exp?: number; iat?: number; } /** * Error response messages for authentication failures. * Using constants ensures these are not flagged as user-controlled content. */ const AUTH_ERROR_MESSAGES = { MISSING_HEADER: 'Missing Authorization header', INVALID_FORMAT: 'Invalid Authorization header format. Expected: Bearer [token]', MISSING_TOKEN: 'Missing token in Authorization header', MISSING_SUBJECT: 'Invalid token: missing subject (sub) claim', TOKEN_EXPIRED: 'Token has expired', VERIFICATION_FAILED: 'Failed to verify authentication token', } as const; /** * Creates JWT authentication middleware. * * @param jwtSecret - The Supabase JWT secret for verification * @returns Express middleware function */ export function createAuthMiddleware(jwtSecret: string) { return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => { const authHeader = req.headers.authorization; if (!authHeader) { res.status(401).json({ error: 'Unauthorized', message: AUTH_ERROR_MESSAGES.MISSING_HEADER, }); return; } if (!authHeader.startsWith('Bearer ')) { res.status(401).json({ error: 'Unauthorized', message: AUTH_ERROR_MESSAGES.INVALID_FORMAT, }); return; } const token = authHeader.slice(7); // Remove 'Bearer ' prefix if (!token) { res.status(401).json({ error: 'Unauthorized', message: AUTH_ERROR_MESSAGES.MISSING_TOKEN, }); return; } try { // Verify and decode the JWT const decoded = jwt.verify(token, jwtSecret, { algorithms: ['HS256'], }) as SupabaseJwtPayload; // Validate required fields if (!decoded.sub) { res.status(401).json({ error: 'Unauthorized', message: AUTH_ERROR_MESSAGES.MISSING_SUBJECT, }); return; } // NOTE: Expiration is already checked by jwt.verify() above. // It throws TokenExpiredError if expired, which is caught below. // Attach user info to request req.user = { userId: decoded.sub, email: decoded.email || null, role: decoded.role || 'authenticated', exp: decoded.exp || 0, }; // Log authenticated request (for audit purposes) console.error(`[AUTH] Authenticated request from user: ${req.user.email || req.user.userId}`); next(); } catch (error) { if (error instanceof jwt.JsonWebTokenError) { // Note: error.message is from the jwt library, not user input res.status(401).json({ error: 'Unauthorized', message: `Invalid token: ${error.message}`, }); return; } if (error instanceof jwt.TokenExpiredError) { res.status(401).json({ error: 'Unauthorized', message: AUTH_ERROR_MESSAGES.TOKEN_EXPIRED, }); return; } console.error('[AUTH] Unexpected error during token verification:', error); res.status(500).json({ error: 'Internal Server Error', message: AUTH_ERROR_MESSAGES.VERIFICATION_FAILED, }); } }; }