Skip to content

Commit 681f14c

Browse files
fix: issue-1593- validate access token on login register (#1595)
1 parent 6371b9c commit 681f14c

File tree

16 files changed

+494
-71
lines changed

16 files changed

+494
-71
lines changed

backend/package-lock.json

Lines changed: 206 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"dotenv": "^16.0.0",
5555
"express": "^4.17.3",
5656
"joi": "^17.6.0",
57+
"jwks-rsa": "^3.1.0",
5758
"jwt-decode": "^3.1.2",
5859
"lint-staged": "^13.0.3",
5960
"moment": "^2.29.4",
@@ -113,4 +114,4 @@
113114
"prettier --write"
114115
]
115116
}
116-
}
117+
}

backend/src/infrastructure/config/configuration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export const configuration = (): Configuration => {
3232
clientSecret: process.env.AZURE_CLIENT_SECRET as string,
3333
tenantId: process.env.AZURE_TENANT_ID as string,
3434
enabled: process.env.AZURE_ENABLE === 'true',
35-
authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`
35+
authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
36+
wellknown: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/.well-known/openid-configuration`
3637
},
3738
smtp: {
3839
host: process.env.SMTP_HOST as string,

backend/src/libs/constants/azure.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export const AZURE_CLIENT_SECRET = 'azure.clientSecret';
77
export const AZURE_TENANT_ID = 'azure.tenantId';
88

99
export const AZURE_AUTHORITY = 'azure.authority';
10+
11+
export const AZURE_WELLKNOWN = 'azure.wellknown';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import faker from '@faker-js/faker';
2+
import { buildTestFactory } from './generic-factory.mock';
3+
import { AzureUserDTO } from 'src/modules/azure/dto/azure-user.dto';
4+
5+
const mockUserData = (): AzureUserDTO => {
6+
const mail = faker.internet.email();
7+
8+
return {
9+
id: faker.datatype.uuid(),
10+
displayName: faker.name.firstName() + faker.name.lastName(),
11+
mail: mail,
12+
userPrincipalName: mail,
13+
createdDateTime: faker.date.past(5),
14+
accountEnabled: faker.datatype.boolean(),
15+
deletedDateTime: faker.datatype.boolean() ? faker.date.recent(1) : null,
16+
employeeLeaveDateTime: faker.datatype.boolean() ? faker.date.recent(1) : null
17+
};
18+
};
19+
20+
export const AzureUserFactory = buildTestFactory<AzureUserDTO>(() => {
21+
return mockUserData();
22+
});
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { DeepMocked, createMock } from '@golevelup/ts-jest';
2+
import { UseCase } from 'src/libs/interfaces/use-case.interface';
3+
import { Test, TestingModule } from '@nestjs/testing';
4+
import LoggedUserDto from 'src/modules/users/dto/logged.user.dto';
5+
import { registerOrLoginUseCase } from '../azure.providers';
6+
import { AUTH_AZURE_SERVICE } from '../constants';
7+
import { AuthAzureServiceInterface } from '../interfaces/services/auth.azure.service.interface';
8+
import { CREATE_USER_SERVICE, GET_USER_SERVICE } from 'src/modules/users/constants';
9+
import { GetUserServiceInterface } from 'src/modules/users/interfaces/services/get.user.service.interface';
10+
import { CreateUserServiceInterface } from 'src/modules/users/interfaces/services/create.user.service.interface';
11+
import { GET_TOKEN_AUTH_SERVICE, UPDATE_USER_SERVICE } from 'src/modules/auth/constants';
12+
import { UpdateUserServiceInterface } from 'src/modules/users/interfaces/services/update.user.service.interface';
13+
import { GetTokenAuthServiceInterface } from 'src/modules/auth/interfaces/services/get-token.auth.service.interface';
14+
import { STORAGE_SERVICE } from 'src/modules/storage/constants';
15+
import { StorageServiceInterface } from 'src/modules/storage/interfaces/services/storage.service';
16+
import { ConfigService } from '@nestjs/config';
17+
import configService from 'src/libs/test-utils/mocks/configService.mock';
18+
import { JwtService } from '@nestjs/jwt';
19+
20+
describe('RegisterOrLoginUserUseCase', () => {
21+
let registerOrLogin: UseCase<string, LoggedUserDto | null>;
22+
let authAzureServiceMock: DeepMocked<AuthAzureServiceInterface>;
23+
let getUserService: DeepMocked<GetUserServiceInterface>;
24+
let updateUserServiceMock: DeepMocked<UpdateUserServiceInterface>;
25+
let tokenServiceMock: DeepMocked<GetTokenAuthServiceInterface>;
26+
27+
beforeAll(async () => {
28+
const module: TestingModule = await Test.createTestingModule({
29+
providers: [
30+
registerOrLoginUseCase,
31+
{
32+
provide: AUTH_AZURE_SERVICE,
33+
useValue: createMock<AuthAzureServiceInterface>()
34+
},
35+
{
36+
provide: GET_USER_SERVICE,
37+
useValue: createMock<GetUserServiceInterface>()
38+
},
39+
{
40+
provide: CREATE_USER_SERVICE,
41+
useValue: createMock<CreateUserServiceInterface>()
42+
},
43+
{
44+
provide: UPDATE_USER_SERVICE,
45+
useValue: createMock<UpdateUserServiceInterface>()
46+
},
47+
{
48+
provide: GET_TOKEN_AUTH_SERVICE,
49+
useValue: createMock<GetTokenAuthServiceInterface>()
50+
},
51+
{
52+
provide: STORAGE_SERVICE,
53+
useValue: createMock<StorageServiceInterface>()
54+
},
55+
{
56+
provide: ConfigService,
57+
useValue: configService
58+
},
59+
{
60+
provide: JwtService,
61+
useValue: createMock<JwtService>()
62+
}
63+
]
64+
}).compile();
65+
66+
registerOrLogin = module.get(registerOrLoginUseCase.provide);
67+
authAzureServiceMock = module.get(AUTH_AZURE_SERVICE);
68+
getUserService = module.get(GET_USER_SERVICE);
69+
updateUserServiceMock = module.get(UPDATE_USER_SERVICE);
70+
tokenServiceMock = module.get(GET_TOKEN_AUTH_SERVICE);
71+
});
72+
73+
beforeEach(() => {
74+
jest.clearAllMocks();
75+
jest.resetAllMocks();
76+
});
77+
78+
it('should be defined', () => {
79+
expect(registerOrLogin).toBeDefined();
80+
});
81+
describe('execute', () => {
82+
it('should return null when validateAccessToken returns false', async () => {
83+
const spy = jest
84+
.spyOn(registerOrLogin, 'validateAccessToken' as any)
85+
.mockResolvedValueOnce(false);
86+
expect(await registerOrLogin.execute('')).toBe(null);
87+
spy.mockRestore();
88+
});
89+
it('should restore user when is deleted and signin normally', async () => {
90+
const spy = jest.spyOn(registerOrLogin, 'validateAccessToken' as any).mockResolvedValueOnce({
91+
unique_name: 'test',
92+
93+
name: 'test',
94+
given_name: 'test',
95+
family_name: 'test'
96+
});
97+
authAzureServiceMock.getUserFromAzure.mockResolvedValueOnce({
98+
accountEnabled: true,
99+
deletedDateTime: null
100+
} as never);
101+
getUserService.getByEmail.mockResolvedValueOnce({
102+
_id: 'id',
103+
104+
isDeleted: true
105+
} as never);
106+
tokenServiceMock.getTokens.mockResolvedValueOnce({} as never);
107+
expect(await registerOrLogin.execute('')).toHaveProperty('email', '[email protected]');
108+
expect(updateUserServiceMock.restoreUser).toHaveBeenCalled();
109+
spy.mockRestore();
110+
});
111+
it('should singIn the user', async () => {
112+
const spy = jest.spyOn(registerOrLogin, 'validateAccessToken' as any).mockResolvedValueOnce({
113+
unique_name: 'test',
114+
115+
name: 'test',
116+
given_name: 'test',
117+
family_name: 'test'
118+
});
119+
authAzureServiceMock.getUserFromAzure.mockResolvedValueOnce({
120+
accountEnabled: true,
121+
deletedDateTime: null
122+
} as never);
123+
getUserService.getByEmail.mockResolvedValueOnce({
124+
_id: 'id',
125+
126+
isDeleted: false
127+
} as never);
128+
tokenServiceMock.getTokens.mockResolvedValueOnce({} as never);
129+
expect(await registerOrLogin.execute('')).toHaveProperty('email', '[email protected]');
130+
spy.mockRestore();
131+
});
132+
});
133+
});

backend/src/modules/azure/applications/register-or-login.azure.use-case.ts

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Inject, Injectable } from '@nestjs/common';
1+
import { Inject, Injectable, Logger } from '@nestjs/common';
22
import { RegisterOrLoginAzureUseCaseInterface } from '../interfaces/applications/register-or-login.azure.use-case.interface';
33
import { AuthAzureServiceInterface } from '../interfaces/services/auth.azure.service.interface';
44
import { AUTH_AZURE_SERVICE } from '../constants';
55
import { AzureDecodedUser } from '../services/auth.azure.service';
6-
import jwt_decode from 'jwt-decode';
76
import { GetUserServiceInterface } from 'src/modules/users/interfaces/services/get.user.service.interface';
87
import { CreateUserServiceInterface } from 'src/modules/users/interfaces/services/create.user.service.interface';
98
import User from 'src/modules/users/entities/user.schema';
@@ -15,9 +14,16 @@ import { StorageServiceInterface } from 'src/modules/storage/interfaces/services
1514
import { CREATE_USER_SERVICE, GET_USER_SERVICE } from 'src/modules/users/constants';
1615
import { STORAGE_SERVICE } from 'src/modules/storage/constants';
1716
import { GET_TOKEN_AUTH_SERVICE, UPDATE_USER_SERVICE } from 'src/modules/auth/constants';
17+
import { JwksClient } from 'jwks-rsa';
18+
import { AZURE_CLIENT_ID, AZURE_WELLKNOWN } from 'src/libs/constants/azure';
19+
import { ConfigService } from '@nestjs/config';
20+
import { JwtService } from '@nestjs/jwt';
21+
import axios from 'axios';
1822

1923
@Injectable()
2024
export class RegisterOrLoginAzureUseCase implements RegisterOrLoginAzureUseCaseInterface {
25+
private readonly logger: Logger = new Logger(RegisterOrLoginAzureUseCase.name);
26+
2127
constructor(
2228
@Inject(AUTH_AZURE_SERVICE)
2329
private readonly authAzureService: AuthAzureServiceInterface,
@@ -30,12 +36,19 @@ export class RegisterOrLoginAzureUseCase implements RegisterOrLoginAzureUseCaseI
3036
@Inject(GET_TOKEN_AUTH_SERVICE)
3137
private readonly getTokenService: GetTokenAuthServiceInterface,
3238
@Inject(STORAGE_SERVICE)
33-
private readonly storageService: StorageServiceInterface
39+
private readonly storageService: StorageServiceInterface,
40+
private readonly configService: ConfigService,
41+
private readonly jwtService: JwtService
3442
) {}
3543

3644
async execute(azureToken: string) {
45+
const validAccessToken = await this.validateAccessToken(azureToken);
46+
47+
if (!validAccessToken) {
48+
return null;
49+
}
3750
const { unique_name, email, name, given_name, family_name } = <AzureDecodedUser>(
38-
jwt_decode(azureToken)
51+
validAccessToken
3952
);
4053

4154
const emailOrUniqueName = email ?? unique_name;
@@ -44,11 +57,23 @@ export class RegisterOrLoginAzureUseCase implements RegisterOrLoginAzureUseCaseI
4457

4558
if (!userFromAzure) return null;
4659

47-
const user = await this.getUserService.getByEmail(emailOrUniqueName);
60+
//This will check if user exists, and if the acount is disabled
61+
if (
62+
!userFromAzure ||
63+
!userFromAzure.accountEnabled ||
64+
(userFromAzure.deletedDateTime !== null && userFromAzure.deletedDateTime <= new Date())
65+
) {
66+
return null;
67+
}
68+
69+
const user = await this.getUserService.getByEmail(emailOrUniqueName, true);
4870

4971
let userToAuthenticate: User;
5072

5173
if (user) {
74+
if (user.isDeleted) {
75+
await this.updateUserService.restoreUser(user._id);
76+
}
5277
userToAuthenticate = user;
5378
} else {
5479
const splitedName = name ? name.split(' ') : [];
@@ -107,4 +132,54 @@ export class RegisterOrLoginAzureUseCase implements RegisterOrLoginAzureUseCaseI
107132
return '';
108133
}
109134
}
135+
136+
/**
137+
* Validate Azure access token using issuer public key
138+
* @param token
139+
* @returns false or decoded token payload
140+
*/
141+
private async validateAccessToken(token: string): Promise<Record<string, any> | boolean> {
142+
try {
143+
//Use wellknown to get issuer and jwks uri's
144+
const wellKnown = this.configService.get(AZURE_WELLKNOWN);
145+
146+
const { data } = await axios.get(wellKnown);
147+
148+
const client = new JwksClient({
149+
jwksUri: data.jwks_uri
150+
});
151+
152+
const { header } = this.jwtService.decode(token, { complete: true }) as {
153+
header: any;
154+
payload: any;
155+
signature: any;
156+
};
157+
158+
if (!header) {
159+
return false;
160+
}
161+
162+
const secret = await client.getSigningKey(header.kid);
163+
164+
const decodedToken = await this.jwtService.verifyAsync(token, {
165+
algorithms: ['RS256'],
166+
audience: this.configService.get(AZURE_CLIENT_ID),
167+
secret: secret.getPublicKey(),
168+
complete: true,
169+
issuer: data.issuer
170+
});
171+
172+
if (decodedToken) {
173+
const { payload } = decodedToken;
174+
175+
return payload;
176+
}
177+
} catch (err) {
178+
this.logger.error(
179+
`An error occurred while validating azure access token. Message: ${err.message}`
180+
);
181+
}
182+
183+
return false;
184+
}
110185
}

backend/src/modules/azure/azure.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { StorageModule } from '../storage/storage.module';
55
import UsersModule from '../users/users.module';
66
import { authAzureService, checkUserUseCase, registerOrLoginUseCase } from './azure.providers';
77
import AzureController from './controller/azure.controller';
8+
import { JwtRegister } from 'src/infrastructure/config/jwt.register';
89

910
@Module({
10-
imports: [UsersModule, AuthModule, CommunicationModule, StorageModule],
11+
imports: [UsersModule, AuthModule, CommunicationModule, StorageModule, JwtRegister],
1112
controllers: [AzureController],
1213
providers: [authAzureService, checkUserUseCase, registerOrLoginUseCase]
1314
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type AzureUserDTO = {
2+
id: string;
3+
mail: string;
4+
displayName: string;
5+
userPrincipalName: string;
6+
createdDateTime: Date;
7+
accountEnabled: boolean;
8+
deletedDateTime: Date | null;
9+
employeeLeaveDateTime: Date | null;
10+
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { AzureUserFound } from '../../services/auth.azure.service';
1+
import { AzureUserDTO } from '../../dto/azure-user.dto';
22

33
export interface AuthAzureServiceInterface {
4-
getUserFromAzure(email: string): Promise<AzureUserFound | undefined>;
4+
getUserFromAzure(email: string): Promise<AzureUserDTO | undefined>;
55
fetchUserPhoto(userId: string): Promise<any>;
66
}

0 commit comments

Comments
 (0)