Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 92 additions & 88 deletions backend/src/authorization/auth-with-api.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
BadRequestException,
HttpException,
Injectable,
InternalServerErrorException,
NestMiddleware,
NotFoundException,
UnauthorizedException,
BadRequestException,
HttpException,
Injectable,
InternalServerErrorException,
NestMiddleware,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user/user.entity.js';
Expand All @@ -23,93 +23,97 @@ import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js';

@Injectable()
export class AuthWithApiMiddleware implements NestMiddleware {
public constructor(
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
) {}
public constructor(
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
) {}

async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise<void> {
try {
await this.authenticateRequest(req);
next();
} catch (error) {
Sentry.captureException(error);
this.handleAuthenticationError(error);
}
}
async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise<void> {
try {
await this.authenticateRequest(req);
next();
} catch (error) {
Sentry.captureException(error);
this.handleAuthenticationError(error);
}
}

private async authenticateRequest(req: IRequestWithCognitoInfo): Promise<void> {
const tokenFromCookie = this.getTokenFromCookie(req);
if (tokenFromCookie) {
await this.authenticateWithToken(tokenFromCookie, req);
} else {
await this.authenticateWithApiKey(req);
}
}
private async authenticateRequest(req: IRequestWithCognitoInfo): Promise<void> {
const tokenFromCookie = this.getTokenFromCookie(req);
if (tokenFromCookie) {
await this.authenticateWithToken(tokenFromCookie, req);
} else {
await this.authenticateWithApiKey(req);
}
}

private getTokenFromCookie(req: Request): string | undefined {
return req.cookies?.[Constants.JWT_COOKIE_KEY_NAME];
}
private getTokenFromCookie(req: Request): string | undefined {
return req.cookies?.[Constants.JWT_COOKIE_KEY_NAME];
}

private handleAuthenticationError(error: any): void {
if (error instanceof HttpException || error instanceof UnauthorizedException) {
throw error;
}
throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED);
}
private handleAuthenticationError(error: any): void {
if (error instanceof HttpException || error instanceof UnauthorizedException) {
throw error;
}
throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED);
}

private async authenticateWithToken(tokenFromCookie: string, req: IRequestWithCognitoInfo): Promise<void> {
try {
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 addedScope: Array<JwtScopesEnum> = data.scope;
if (addedScope && addedScope.length > 0) {
if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) {
throw new BadRequestException(Messages.TWO_FA_REQUIRED);
}
}
private async authenticateWithToken(tokenFromCookie: string, req: IRequestWithCognitoInfo): Promise<void> {
try {
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 addedScope: Array<JwtScopesEnum> = 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;
} catch (error) {
Sentry.captureException(error);
throw error;
}
}
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;
} catch (error) {
Sentry.captureException(error);
throw error;
}
}

private async authenticateWithApiKey(req: IRequestWithCognitoInfo): Promise<void> {
let apiKey = req.headers?.['x-api-key'];
if (Array.isArray(apiKey)) {
apiKey = apiKey[0];
}
if (!apiKey) {
throw new UnauthorizedException(Messages.NO_AUTH_KEYS_FOUND);
}
const apiKeyHash = await Encryptor.processDataWithAlgorithm(apiKey, EncryptionAlgorithmEnum.sha256);
const foundUserByApiKey = await this.userRepository
.createQueryBuilder('user')
.innerJoinAndSelect('user.api_keys', 'api_key')
.where('api_key.hash = :hash', { hash: apiKeyHash })
.getOne();
private async authenticateWithApiKey(req: IRequestWithCognitoInfo): Promise<void> {
let apiKey = req.headers?.['x-api-key'];
if (Array.isArray(apiKey)) {
apiKey = apiKey[0];
}
if (!apiKey) {
throw new UnauthorizedException(Messages.NO_AUTH_KEYS_FOUND);
}
const apiKeyHash = await Encryptor.processDataWithAlgorithm(apiKey, EncryptionAlgorithmEnum.sha256);
const foundUserByApiKey = await this.userRepository
.createQueryBuilder('user')
.innerJoinAndSelect('user.api_keys', 'api_key')
.where('api_key.hash = :hash', { hash: apiKeyHash })
.getOne();

if (!foundUserByApiKey) {
throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND);
}
req.decoded = {
sub: foundUserByApiKey.id,
email: foundUserByApiKey.email,
};
}
if (foundUserByApiKey.suspended) {
throw new UnauthorizedException(Messages.API_KEY_SUSPENDED);
}

if (!foundUserByApiKey) {
throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND);
}
Comment on lines +107 to +113

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check for foundUserByApiKey must be performed before accessing its suspended property. Currently, if no user is found with the provided API key, the code will throw a runtime error when trying to access .suspended on a null object at line 107. The null check on lines 111-113 should be moved before the suspended check on lines 107-109.

Suggested change
if (foundUserByApiKey.suspended) {
throw new UnauthorizedException(Messages.API_KEY_SUSPENDED);
}
if (!foundUserByApiKey) {
throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND);
}
if (!foundUserByApiKey) {
throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND);
}
if (foundUserByApiKey.suspended) {
throw new UnauthorizedException(Messages.API_KEY_SUSPENDED);
}

Copilot uses AI. Check for mistakes.
req.decoded = {
sub: foundUserByApiKey.id,
email: foundUserByApiKey.email,
};
}
}
1 change: 1 addition & 0 deletions backend/src/exceptions/text/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TableActionMethodEnum } from '../../enums/table-action-method-enum.js';
import { enumToString } from '../../helpers/enum-to-string.js';
import { toPrettyErrorsMsg } from '../../helpers/index.js';
export const Messages = {
API_KEY_SUSPENDED: 'API key is suspended',
AI_REQUESTS_NOT_ALLOWED: 'AI requests are not allowed for this connection',
AI_THREAD_NOT_FOUND: 'Thread with specified parameters not found',
ACCOUNT_SUSPENDED:
Expand Down
Loading