From 529fdf60c50632ad7ad0c0f0d54abaa3ea8d8286 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Wed, 28 Jan 2026 18:14:00 +0000 Subject: [PATCH] Enhance JWT verification in AuthMiddlewares and update test assertions for unauthorized access --- .../authorization/auth-with-api.middleware.ts | 18 ++- backend/src/authorization/auth.middleware.ts | 130 ++++++++++-------- .../non-saas-company-info-e2e.test.ts | 2 +- .../non-saas-tests/non-saas-user-e2e.test.ts | 2 +- .../saas-tests/company-info-e2e.test.ts | 4 +- .../ava-tests/saas-tests/user-e2e.test.ts | 2 +- 6 files changed, 91 insertions(+), 67 deletions(-) diff --git a/backend/src/authorization/auth-with-api.middleware.ts b/backend/src/authorization/auth-with-api.middleware.ts index 4e0b98505..e00346931 100644 --- a/backend/src/authorization/auth-with-api.middleware.ts +++ b/backend/src/authorization/auth-with-api.middleware.ts @@ -63,9 +63,20 @@ export class AuthWithApiMiddleware implements NestMiddleware { const jwtSecret = process.env.JWT_SECRET; const data = jwt.verify(tokenFromCookie, jwtSecret) as jwt.JwtPayload; const userId = data.id; + if (!userId) { throw new UnauthorizedException('JWT verification failed'); } + + const userExists = await this.userRepository.findOne({ where: { id: userId } }); + if (!userExists) { + throw new UnauthorizedException('JWT verification failed'); + } + + if (userExists.suspended) { + throw new UnauthorizedException(Messages.ACCOUNT_SUSPENDED); + } + const addedScope: Array = data.scope; if (addedScope && addedScope.length > 0) { if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) { @@ -104,13 +115,14 @@ export class AuthWithApiMiddleware implements NestMiddleware { .where('api_key.hash = :hash', { hash: apiKeyHash }) .getOne(); + if (!foundUserByApiKey) { + throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND); + } + if (foundUserByApiKey.suspended) { throw new UnauthorizedException(Messages.API_KEY_SUSPENDED); } - if (!foundUserByApiKey) { - throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND); - } req.decoded = { sub: foundUserByApiKey.id, email: foundUserByApiKey.email, diff --git a/backend/src/authorization/auth.middleware.ts b/backend/src/authorization/auth.middleware.ts index b671a9249..d3cbd61da 100644 --- a/backend/src/authorization/auth.middleware.ts +++ b/backend/src/authorization/auth.middleware.ts @@ -1,10 +1,10 @@ import { - BadRequestException, - HttpException, - Injectable, - InternalServerErrorException, - NestMiddleware, - UnauthorizedException, + BadRequestException, + HttpException, + Injectable, + InternalServerErrorException, + NestMiddleware, + UnauthorizedException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; @@ -21,61 +21,73 @@ import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js'; @Injectable() export class AuthMiddleware implements NestMiddleware { - public constructor( - @InjectRepository(UserEntity)readonly _userRepository: Repository, - @InjectRepository(LogOutEntity) - private readonly logOutRepository: Repository, - ) {} - async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise { - let token: string; - try { - token = req.cookies[Constants.JWT_COOKIE_KEY_NAME]; - } catch (_e) { - if (process.env.NODE_ENV !== 'test') { - throw new UnauthorizedException('JWT verification failed'); - } - } + public constructor( + @InjectRepository(UserEntity) + private readonly userRepository: Repository, + @InjectRepository(LogOutEntity) + private readonly logOutRepository: Repository, + ) {} + async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise { + let token: string; + try { + token = req.cookies[Constants.JWT_COOKIE_KEY_NAME]; + } catch (_e) { + if (process.env.NODE_ENV !== 'test') { + throw new UnauthorizedException('JWT verification failed'); + } + } - if (!token) { - throw new UnauthorizedException('Token is missing'); - } + if (!token) { + throw new UnauthorizedException('Token is missing'); + } - const isLoggedOut = !!(await this.logOutRepository.findOne({ where: { jwtToken: token } })); - if (isLoggedOut) { - throw new UnauthorizedException('Token is invalid'); - } + const isLoggedOut = !!(await this.logOutRepository.findOne({ where: { jwtToken: token } })); + if (isLoggedOut) { + throw new UnauthorizedException('Token is invalid'); + } - try { - const jwtSecret = process.env.JWT_SECRET; - const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload; - const userId = data.id; - if (!userId) { - throw new UnauthorizedException('JWT verification failed'); - } - const addedScope: Array = data.scope; - if (addedScope && addedScope.length > 0) { - if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) { - throw new BadRequestException(Messages.TWO_FA_REQUIRED); - } - } + try { + const jwtSecret = process.env.JWT_SECRET; + const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload; + const userId = data.id; - const payload = { - sub: userId, - email: data.email, - exp: data.exp, - iat: data.iat, - }; - if (!payload || isObjectEmpty(payload)) { - throw new UnauthorizedException('JWT verification failed'); - } - req.decoded = payload; - next(); - } catch (e) { - Sentry.captureException(e); - if (e instanceof HttpException || e instanceof UnauthorizedException) { - throw e; - } - throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED); - } - } + if (!userId) { + throw new UnauthorizedException('JWT verification failed'); + } + + const userExists = await this.userRepository.findOne({ where: { id: userId } }); + if (!userExists) { + throw new UnauthorizedException('JWT verification failed'); + } + + if (userExists.suspended) { + throw new UnauthorizedException(Messages.ACCOUNT_SUSPENDED); + } + + const addedScope: Array = data.scope; + if (addedScope && addedScope.length > 0) { + if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) { + throw new BadRequestException(Messages.TWO_FA_REQUIRED); + } + } + + const payload = { + sub: userId, + email: data.email, + exp: data.exp, + iat: data.iat, + }; + if (!payload || isObjectEmpty(payload)) { + throw new UnauthorizedException('JWT verification failed'); + } + req.decoded = payload; + next(); + } catch (e) { + Sentry.captureException(e); + if (e instanceof HttpException || e instanceof UnauthorizedException) { + throw e; + } + throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED); + } + } } diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-company-info-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-company-info-e2e.test.ts index 2b92b891e..342795530 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-company-info-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-company-info-e2e.test.ts @@ -658,5 +658,5 @@ test.serial(`${currentTest} should delete company`, async (t) => { .set('Cookie', adminUserToken) .set('Accept', 'application/json'); - t.is(foundCompanyInfoAfterDelete.status, 403); + t.is(foundCompanyInfoAfterDelete.status, 401); }); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-e2e.test.ts index 42a8d7011..070d0256b 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-user-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-e2e.test.ts @@ -95,7 +95,7 @@ test.serial(`${currentTest} should return user deletion result`, async (t) => { .set('Cookie', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getUserResult.status, 404); + t.is(getUserResult.status, 401); t.pass(); }); diff --git a/backend/test/ava-tests/saas-tests/company-info-e2e.test.ts b/backend/test/ava-tests/saas-tests/company-info-e2e.test.ts index 13954e5c9..52d7b9211 100644 --- a/backend/test/ava-tests/saas-tests/company-info-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/company-info-e2e.test.ts @@ -726,7 +726,7 @@ test.serial(`${currentTest} should delete company`, async (t) => { .set('Cookie', adminUserToken) .set('Accept', 'application/json'); - t.is(foundCompanyInfoAfterDelete.status, 403); + t.is(foundCompanyInfoAfterDelete.status, 401); }); currentTest = `PUT company/2fa/:companyId`; @@ -905,7 +905,7 @@ test.serial( .set('Accept', 'application/json'); const findAllConnectionsResponseRO = JSON.parse(findAllConnectionsResponse.text); - t.is(findAllConnectionsResponse.status, 403); + t.is(findAllConnectionsResponse.status, 401); t.is(findAllConnectionsResponseRO.message, Messages.ACCOUNT_SUSPENDED); }, ); diff --git a/backend/test/ava-tests/saas-tests/user-e2e.test.ts b/backend/test/ava-tests/saas-tests/user-e2e.test.ts index e3242b4a3..d644c158e 100644 --- a/backend/test/ava-tests/saas-tests/user-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/user-e2e.test.ts @@ -105,7 +105,7 @@ test.serial(`${currentTest} should return user deletion result`, async (t) => { .set('Cookie', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getUserResult.status, 404); + t.is(getUserResult.status, 401); t.pass(); });