Skip to content

Commit be945b7

Browse files
Merge branch 'main' into table-view-fixes
2 parents 9a9ba67 + e714cd3 commit be945b7

7 files changed

Lines changed: 181 additions & 152 deletions

File tree

Lines changed: 104 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
2-
BadRequestException,
3-
HttpException,
4-
Injectable,
5-
InternalServerErrorException,
6-
NestMiddleware,
7-
NotFoundException,
8-
UnauthorizedException,
2+
BadRequestException,
3+
HttpException,
4+
Injectable,
5+
InternalServerErrorException,
6+
NestMiddleware,
7+
NotFoundException,
8+
UnauthorizedException,
99
} from '@nestjs/common';
1010
import { InjectRepository } from '@nestjs/typeorm';
1111
import { UserEntity } from '../entities/user/user.entity.js';
@@ -23,93 +23,109 @@ import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js';
2323

2424
@Injectable()
2525
export class AuthWithApiMiddleware implements NestMiddleware {
26-
public constructor(
27-
@InjectRepository(UserEntity)
28-
private readonly userRepository: Repository<UserEntity>,
29-
) {}
26+
public constructor(
27+
@InjectRepository(UserEntity)
28+
private readonly userRepository: Repository<UserEntity>,
29+
) {}
3030

31-
async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise<void> {
32-
try {
33-
await this.authenticateRequest(req);
34-
next();
35-
} catch (error) {
36-
Sentry.captureException(error);
37-
this.handleAuthenticationError(error);
38-
}
39-
}
31+
async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise<void> {
32+
try {
33+
await this.authenticateRequest(req);
34+
next();
35+
} catch (error) {
36+
Sentry.captureException(error);
37+
this.handleAuthenticationError(error);
38+
}
39+
}
4040

41-
private async authenticateRequest(req: IRequestWithCognitoInfo): Promise<void> {
42-
const tokenFromCookie = this.getTokenFromCookie(req);
43-
if (tokenFromCookie) {
44-
await this.authenticateWithToken(tokenFromCookie, req);
45-
} else {
46-
await this.authenticateWithApiKey(req);
47-
}
48-
}
41+
private async authenticateRequest(req: IRequestWithCognitoInfo): Promise<void> {
42+
const tokenFromCookie = this.getTokenFromCookie(req);
43+
if (tokenFromCookie) {
44+
await this.authenticateWithToken(tokenFromCookie, req);
45+
} else {
46+
await this.authenticateWithApiKey(req);
47+
}
48+
}
4949

50-
private getTokenFromCookie(req: Request): string | undefined {
51-
return req.cookies?.[Constants.JWT_COOKIE_KEY_NAME];
52-
}
50+
private getTokenFromCookie(req: Request): string | undefined {
51+
return req.cookies?.[Constants.JWT_COOKIE_KEY_NAME];
52+
}
5353

54-
private handleAuthenticationError(error: any): void {
55-
if (error instanceof HttpException || error instanceof UnauthorizedException) {
56-
throw error;
57-
}
58-
throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED);
59-
}
54+
private handleAuthenticationError(error: any): void {
55+
if (error instanceof HttpException || error instanceof UnauthorizedException) {
56+
throw error;
57+
}
58+
throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED);
59+
}
6060

61-
private async authenticateWithToken(tokenFromCookie: string, req: IRequestWithCognitoInfo): Promise<void> {
62-
try {
63-
const jwtSecret = process.env.JWT_SECRET;
64-
const data = jwt.verify(tokenFromCookie, jwtSecret) as jwt.JwtPayload;
65-
const userId = data.id;
66-
if (!userId) {
67-
throw new UnauthorizedException('JWT verification failed');
68-
}
69-
const addedScope: Array<JwtScopesEnum> = data.scope;
70-
if (addedScope && addedScope.length > 0) {
71-
if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) {
72-
throw new BadRequestException(Messages.TWO_FA_REQUIRED);
73-
}
74-
}
61+
private async authenticateWithToken(tokenFromCookie: string, req: IRequestWithCognitoInfo): Promise<void> {
62+
try {
63+
const jwtSecret = process.env.JWT_SECRET;
64+
const data = jwt.verify(tokenFromCookie, jwtSecret) as jwt.JwtPayload;
65+
const userId = data.id;
7566

76-
const payload = {
77-
sub: userId,
78-
email: data.email,
79-
exp: data.exp,
80-
iat: data.iat,
81-
};
82-
if (!payload || isObjectEmpty(payload)) {
83-
throw new UnauthorizedException('JWT verification failed');
84-
}
85-
req.decoded = payload;
86-
} catch (error) {
87-
Sentry.captureException(error);
88-
throw error;
89-
}
90-
}
67+
if (!userId) {
68+
throw new UnauthorizedException('JWT verification failed');
69+
}
9170

92-
private async authenticateWithApiKey(req: IRequestWithCognitoInfo): Promise<void> {
93-
let apiKey = req.headers?.['x-api-key'];
94-
if (Array.isArray(apiKey)) {
95-
apiKey = apiKey[0];
96-
}
97-
if (!apiKey) {
98-
throw new UnauthorizedException(Messages.NO_AUTH_KEYS_FOUND);
99-
}
100-
const apiKeyHash = await Encryptor.processDataWithAlgorithm(apiKey, EncryptionAlgorithmEnum.sha256);
101-
const foundUserByApiKey = await this.userRepository
102-
.createQueryBuilder('user')
103-
.innerJoinAndSelect('user.api_keys', 'api_key')
104-
.where('api_key.hash = :hash', { hash: apiKeyHash })
105-
.getOne();
71+
const userExists = await this.userRepository.findOne({ where: { id: userId } });
72+
if (!userExists) {
73+
throw new UnauthorizedException('JWT verification failed');
74+
}
10675

107-
if (!foundUserByApiKey) {
108-
throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND);
109-
}
110-
req.decoded = {
111-
sub: foundUserByApiKey.id,
112-
email: foundUserByApiKey.email,
113-
};
114-
}
76+
if (userExists.suspended) {
77+
throw new UnauthorizedException(Messages.ACCOUNT_SUSPENDED);
78+
}
79+
80+
const addedScope: Array<JwtScopesEnum> = data.scope;
81+
if (addedScope && addedScope.length > 0) {
82+
if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) {
83+
throw new BadRequestException(Messages.TWO_FA_REQUIRED);
84+
}
85+
}
86+
87+
const payload = {
88+
sub: userId,
89+
email: data.email,
90+
exp: data.exp,
91+
iat: data.iat,
92+
};
93+
if (!payload || isObjectEmpty(payload)) {
94+
throw new UnauthorizedException('JWT verification failed');
95+
}
96+
req.decoded = payload;
97+
} catch (error) {
98+
Sentry.captureException(error);
99+
throw error;
100+
}
101+
}
102+
103+
private async authenticateWithApiKey(req: IRequestWithCognitoInfo): Promise<void> {
104+
let apiKey = req.headers?.['x-api-key'];
105+
if (Array.isArray(apiKey)) {
106+
apiKey = apiKey[0];
107+
}
108+
if (!apiKey) {
109+
throw new UnauthorizedException(Messages.NO_AUTH_KEYS_FOUND);
110+
}
111+
const apiKeyHash = await Encryptor.processDataWithAlgorithm(apiKey, EncryptionAlgorithmEnum.sha256);
112+
const foundUserByApiKey = await this.userRepository
113+
.createQueryBuilder('user')
114+
.innerJoinAndSelect('user.api_keys', 'api_key')
115+
.where('api_key.hash = :hash', { hash: apiKeyHash })
116+
.getOne();
117+
118+
if (!foundUserByApiKey) {
119+
throw new NotFoundException(Messages.NO_AUTH_KEYS_FOUND);
120+
}
121+
122+
if (foundUserByApiKey.suspended) {
123+
throw new UnauthorizedException(Messages.API_KEY_SUSPENDED);
124+
}
125+
126+
req.decoded = {
127+
sub: foundUserByApiKey.id,
128+
email: foundUserByApiKey.email,
129+
};
130+
}
115131
}
Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
2-
BadRequestException,
3-
HttpException,
4-
Injectable,
5-
InternalServerErrorException,
6-
NestMiddleware,
7-
UnauthorizedException,
2+
BadRequestException,
3+
HttpException,
4+
Injectable,
5+
InternalServerErrorException,
6+
NestMiddleware,
7+
UnauthorizedException,
88
} from '@nestjs/common';
99
import { InjectRepository } from '@nestjs/typeorm';
1010
import { Response } from 'express';
@@ -21,61 +21,73 @@ import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js';
2121

2222
@Injectable()
2323
export class AuthMiddleware implements NestMiddleware {
24-
public constructor(
25-
@InjectRepository(UserEntity)readonly _userRepository: Repository<UserEntity>,
26-
@InjectRepository(LogOutEntity)
27-
private readonly logOutRepository: Repository<LogOutEntity>,
28-
) {}
29-
async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise<void> {
30-
let token: string;
31-
try {
32-
token = req.cookies[Constants.JWT_COOKIE_KEY_NAME];
33-
} catch (_e) {
34-
if (process.env.NODE_ENV !== 'test') {
35-
throw new UnauthorizedException('JWT verification failed');
36-
}
37-
}
24+
public constructor(
25+
@InjectRepository(UserEntity)
26+
private readonly userRepository: Repository<UserEntity>,
27+
@InjectRepository(LogOutEntity)
28+
private readonly logOutRepository: Repository<LogOutEntity>,
29+
) {}
30+
async use(req: IRequestWithCognitoInfo, _res: Response, next: (err?: any, res?: any) => void): Promise<void> {
31+
let token: string;
32+
try {
33+
token = req.cookies[Constants.JWT_COOKIE_KEY_NAME];
34+
} catch (_e) {
35+
if (process.env.NODE_ENV !== 'test') {
36+
throw new UnauthorizedException('JWT verification failed');
37+
}
38+
}
3839

39-
if (!token) {
40-
throw new UnauthorizedException('Token is missing');
41-
}
40+
if (!token) {
41+
throw new UnauthorizedException('Token is missing');
42+
}
4243

43-
const isLoggedOut = !!(await this.logOutRepository.findOne({ where: { jwtToken: token } }));
44-
if (isLoggedOut) {
45-
throw new UnauthorizedException('Token is invalid');
46-
}
44+
const isLoggedOut = !!(await this.logOutRepository.findOne({ where: { jwtToken: token } }));
45+
if (isLoggedOut) {
46+
throw new UnauthorizedException('Token is invalid');
47+
}
4748

48-
try {
49-
const jwtSecret = process.env.JWT_SECRET;
50-
const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload;
51-
const userId = data.id;
52-
if (!userId) {
53-
throw new UnauthorizedException('JWT verification failed');
54-
}
55-
const addedScope: Array<JwtScopesEnum> = data.scope;
56-
if (addedScope && addedScope.length > 0) {
57-
if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) {
58-
throw new BadRequestException(Messages.TWO_FA_REQUIRED);
59-
}
60-
}
49+
try {
50+
const jwtSecret = process.env.JWT_SECRET;
51+
const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload;
52+
const userId = data.id;
6153

62-
const payload = {
63-
sub: userId,
64-
email: data.email,
65-
exp: data.exp,
66-
iat: data.iat,
67-
};
68-
if (!payload || isObjectEmpty(payload)) {
69-
throw new UnauthorizedException('JWT verification failed');
70-
}
71-
req.decoded = payload;
72-
next();
73-
} catch (e) {
74-
Sentry.captureException(e);
75-
if (e instanceof HttpException || e instanceof UnauthorizedException) {
76-
throw e;
77-
}
78-
throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED);
79-
}
80-
}
54+
if (!userId) {
55+
throw new UnauthorizedException('JWT verification failed');
56+
}
57+
58+
const userExists = await this.userRepository.findOne({ where: { id: userId } });
59+
if (!userExists) {
60+
throw new UnauthorizedException('JWT verification failed');
61+
}
62+
63+
if (userExists.suspended) {
64+
throw new UnauthorizedException(Messages.ACCOUNT_SUSPENDED);
65+
}
66+
67+
const addedScope: Array<JwtScopesEnum> = data.scope;
68+
if (addedScope && addedScope.length > 0) {
69+
if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) {
70+
throw new BadRequestException(Messages.TWO_FA_REQUIRED);
71+
}
72+
}
73+
74+
const payload = {
75+
sub: userId,
76+
email: data.email,
77+
exp: data.exp,
78+
iat: data.iat,
79+
};
80+
if (!payload || isObjectEmpty(payload)) {
81+
throw new UnauthorizedException('JWT verification failed');
82+
}
83+
req.decoded = payload;
84+
next();
85+
} catch (e) {
86+
Sentry.captureException(e);
87+
if (e instanceof HttpException || e instanceof UnauthorizedException) {
88+
throw e;
89+
}
90+
throw new InternalServerErrorException(Messages.AUTHORIZATION_REJECTED);
91+
}
92+
}
8193
}

backend/src/exceptions/text/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TableActionMethodEnum } from '../../enums/table-action-method-enum.js';
1313
import { enumToString } from '../../helpers/enum-to-string.js';
1414
import { toPrettyErrorsMsg } from '../../helpers/index.js';
1515
export const Messages = {
16+
API_KEY_SUSPENDED: 'API key is suspended',
1617
AI_REQUESTS_NOT_ALLOWED: 'AI requests are not allowed for this connection',
1718
AI_THREAD_NOT_FOUND: 'Thread with specified parameters not found',
1819
ACCOUNT_SUSPENDED:

backend/test/ava-tests/non-saas-tests/non-saas-company-info-e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,5 +658,5 @@ test.serial(`${currentTest} should delete company`, async (t) => {
658658
.set('Cookie', adminUserToken)
659659
.set('Accept', 'application/json');
660660

661-
t.is(foundCompanyInfoAfterDelete.status, 403);
661+
t.is(foundCompanyInfoAfterDelete.status, 401);
662662
});

backend/test/ava-tests/non-saas-tests/non-saas-user-e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ test.serial(`${currentTest} should return user deletion result`, async (t) => {
9595
.set('Cookie', token)
9696
.set('Content-Type', 'application/json')
9797
.set('Accept', 'application/json');
98-
t.is(getUserResult.status, 404);
98+
t.is(getUserResult.status, 401);
9999
t.pass();
100100
});
101101

backend/test/ava-tests/saas-tests/company-info-e2e.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ test.serial(`${currentTest} should delete company`, async (t) => {
726726
.set('Cookie', adminUserToken)
727727
.set('Accept', 'application/json');
728728

729-
t.is(foundCompanyInfoAfterDelete.status, 403);
729+
t.is(foundCompanyInfoAfterDelete.status, 401);
730730
});
731731

732732
currentTest = `PUT company/2fa/:companyId`;
@@ -905,7 +905,7 @@ test.serial(
905905
.set('Accept', 'application/json');
906906

907907
const findAllConnectionsResponseRO = JSON.parse(findAllConnectionsResponse.text);
908-
t.is(findAllConnectionsResponse.status, 403);
908+
t.is(findAllConnectionsResponse.status, 401);
909909
t.is(findAllConnectionsResponseRO.message, Messages.ACCOUNT_SUSPENDED);
910910
},
911911
);

0 commit comments

Comments
 (0)