import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { NextFunction, Request, Response } from 'express'; export const CLIENT_APP_HEADER = 'x-client-app'; // EUserType values from @tria-plc/api-common, duplicated here to avoid // pulling in the full enum just for this string comparison. const ALLOWED_USER_TYPES_BY_CLIENT: Record = { backoffice: ['employee'], portal: ['individual', 'external_organization'], }; /** * Blocks EDRFREIGHT-415: /auth/login and /auth/mfa-verify match credentials * against email/username/phone_number only (see vendor * findUserForLogin), with no check that the account's userType belongs on * the app that's asking. A backoffice (employee) client presenting a * customer's credentials — or vice versa — must not get a session. */ @Injectable() export class LoginAudienceMiddleware implements NestMiddleware { constructor(@InjectDataSource() private readonly dataSource: DataSource) {} async use(req: Request, _res: Response, next: NextFunction) { const clientApp = req.header(CLIENT_APP_HEADER); const allowedUserTypes = clientApp ? ALLOWED_USER_TYPES_BY_CLIENT[clientApp] : undefined; if (!allowedUserTypes) { throw new ForbiddenException( `Missing or unrecognized ${CLIENT_APP_HEADER} header`, ); } const identifier: unknown = req.body?.email; if (typeof identifier !== 'string' || !identifier) { // No identifier to look up — the vendor DTO validation rejects the // request on its own. return next(); } const [user] = await this.dataSource.query( `SELECT user_type AS "userType" FROM iam.users WHERE email = $1 OR username = $1 OR phone_number = $1 LIMIT 1`, [identifier], ); if (user && !allowedUserTypes.includes(user.userType)) { throw new ForbiddenException( `This account cannot sign in through the ${clientApp} application`, ); } next(); } }