Skip to content

Commit 7db058f

Browse files
committed
feat: implement MFA enhancements and agent authentication improvements
- Added MFA (Multi-Factor Authentication) support to the authentication process, requiring verification via TOTP (Time-based One-Time Password). - Introduced a challenge mechanism for MFA, allowing users to complete authentication with an OTP code. - Enhanced the agent controller to sanitize payloads and include MFA requirements for sensitive operations. - Updated the authentication service to handle MFA challenges and verify OTP codes, improving security during login. - Implemented additional checks for agent states and IP validation to ensure only authorized access. - Refactored various controllers to enforce MFA on critical actions, enhancing overall application security.
1 parent 47c63ec commit 7db058f

32 files changed

Lines changed: 2038 additions & 545 deletions
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { applyDecorators, SetMetadata } from '@nestjs/common';
2+
3+
export const META_REQUIRE_MFA = 'require-mfa';
4+
export const RequireMfa = () => applyDecorators(SetMetadata(META_REQUIRE_MFA, true));

apps/api/src/_common/functions/resolve-client-ip.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,6 @@ export function buildClientIpDebugPayload(req: Request): Record<string, unknown>
8989
const xffFirst = normalizeIp(firstCsvSegment(xffRaw) ?? (xffRaw?.trim() || null));
9090
const xffEchoesPeer = Boolean(peerIp && xffFirst && peerIp === xffFirst);
9191
const clientIp = resolveClientIp(req);
92-
const hasTrustedForward =
93-
Boolean(normalizeIp(headerString(req, 'x-real-ip'))) ||
94-
Boolean(normalizeIp(headerString(req, 'cf-connecting-ip'))) ||
95-
Boolean(normalizeIp(headerString(req, 'true-client-ip')));
96-
97-
let hintFr =
98-
'clientIp = valeur utilisée par Nest (auth, audits). Ce n’est pas forcément « votre PC » si la connexion TCP passe par un relai (tunnel, port forward, Docker/Nitro).';
99-
100-
if (xffEchoesPeer && !hasTrustedForward) {
101-
hintFr +=
102-
' Ici X-Forwarded-For ne fait que répéter l’IP du pair TCP (relai) : il est ignoré pour la résolution. Sans X-Real-IP / CF-Connecting-IP / premier hop X-Forwarded-For fiable, Nest ne peut pas inventer votre IP LAN. Solution : reverse-proxy (nginx, Traefik…) qui transmet le client, puis SESAME_TRUST_PROXY=1 sur l’API.';
103-
}
10492

10593
return {
10694
clientIp,
@@ -115,6 +103,5 @@ export function buildClientIpDebugPayload(req: Request): Record<string, unknown>
115103
cfConnectingIp: pick('cf-connecting-ip') ?? null,
116104
host: pick('host') ?? null,
117105
trustProxyEnv: process.env['SESAME_TRUST_PROXY'] ?? null,
118-
hintFr,
119106
};
120107
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
2+
import { Reflector } from '@nestjs/core';
3+
import { META_REQUIRE_MFA } from '~/_common/decorators/require-mfa.decorator';
4+
5+
@Injectable()
6+
export class MfaGuard implements CanActivate {
7+
public constructor(private readonly reflector: Reflector) {}
8+
9+
public canActivate(context: ExecutionContext): boolean {
10+
const requiresMfa = this.reflector.getAllAndOverride<boolean>(META_REQUIRE_MFA, [
11+
context.getClass(),
12+
context.getHandler(),
13+
]);
14+
if (!requiresMfa) return true;
15+
16+
const request = context.switchToHttp().getRequest<{ user?: { mfaVerified?: boolean } }>();
17+
if (request?.user?.mfaVerified) return true;
18+
throw new ForbiddenException('MFA required');
19+
}
20+
}

apps/api/src/app.module.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { isConsoleEntrypoint } from './_common/functions/is-cli';
3232
import { AccessControlModule, ACGuard, RolesBuilder } from 'nest-access-control';
3333
import { AcGuard } from './_common/guards/ac.guard';
3434
import { AclRuntimeService } from './core/roles/acl-runtime.service';
35+
import { MfaGuard } from './_common/guards/mfa.guard';
3536

3637
@Module({
3738
imports: [
@@ -128,7 +129,7 @@ import { AclRuntimeService } from './core/roles/acl-runtime.service';
128129
imports: [CoreModule],
129130
inject: [AclRuntimeService],
130131
useFactory: async (service: AclRuntimeService): Promise<RolesBuilder> => {
131-
return await service.getGuardRolesBuilder()
132+
return await service.getGuardRolesBuilder();
132133
},
133134
}),
134135
FactorydriveModule.forRootAsync({
@@ -165,6 +166,10 @@ import { AclRuntimeService } from './core/roles/acl-runtime.service';
165166
provide: APP_GUARD,
166167
useClass: AcGuard(),
167168
},
169+
{
170+
provide: APP_GUARD,
171+
useClass: MfaGuard,
172+
},
168173
// {
169174
// provide: APP_FILTER,
170175
// useClass: AllExceptionFilter,
@@ -183,4 +188,4 @@ import { AclRuntimeService } from './core/roles/acl-runtime.service';
183188
},
184189
],
185190
})
186-
export class AppModule { }
191+
export class AppModule {}

apps/api/src/core/agents/_dto/agents.dto.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ApiProperty, PartialType } from '@nestjs/swagger'
2-
import { IsString, IsNotEmpty, ValidateNested, IsEmail, IsBoolean, IsMongoId, IsOptional } from 'class-validator'
3-
import { Type } from 'class-transformer'
4-
import { StatePartDTO } from './parts/state.part.dto'
5-
import { SecurityPartDTO } from './parts/security.part.dto'
6-
import { CustomFieldsDto } from '~/_common/abstracts/dto/custom-fields.dto'
1+
import { ApiProperty, PartialType } from '@nestjs/swagger';
2+
import { IsString, IsNotEmpty, ValidateNested, IsEmail, IsBoolean, IsMongoId, IsOptional } from 'class-validator';
3+
import { Type } from 'class-transformer';
4+
import { StatePartDTO } from './parts/state.part.dto';
5+
import { SecurityPartDTO } from './parts/security.part.dto';
6+
import { CustomFieldsDto } from '~/_common/abstracts/dto/custom-fields.dto';
77

88
/**
99
* DTO pour la création d'un agent.
@@ -56,7 +56,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
5656
@IsString()
5757
@IsNotEmpty()
5858
@ApiProperty()
59-
public username: string
59+
public username: string;
6060

6161
/**
6262
* Nom d'affichage de l'agent.
@@ -67,7 +67,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
6767
@IsString()
6868
@IsOptional()
6969
@ApiProperty()
70-
public displayName?: string
70+
public displayName?: string;
7171

7272
/**
7373
* Adresse email unique de l'agent.
@@ -79,7 +79,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
7979
@IsEmail()
8080
@IsNotEmpty()
8181
@ApiProperty()
82-
public email: string
82+
public email: string;
8383

8484
/**
8585
* Mot de passe de l'agent.
@@ -91,7 +91,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
9191
@IsString()
9292
@IsNotEmpty()
9393
@ApiProperty()
94-
public password: string
94+
public password: string;
9595

9696
/**
9797
* Méthode d'authentification tierce.
@@ -104,7 +104,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
104104
@IsString()
105105
@IsOptional()
106106
@ApiProperty()
107-
public thirdPartyAuth?: string
107+
public thirdPartyAuth?: string;
108108

109109
/**
110110
* État de lifecycle de l'agent.
@@ -116,7 +116,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
116116
@Type(() => StatePartDTO)
117117
@IsNotEmpty()
118118
@ApiProperty({ type: StatePartDTO })
119-
public state: StatePartDTO
119+
public state: StatePartDTO;
120120

121121
/**
122122
* URL de base pour l'agent.
@@ -128,7 +128,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
128128
@IsString()
129129
@IsOptional()
130130
@ApiProperty()
131-
public baseURL?: string
131+
public baseURL?: string;
132132

133133
/**
134134
* Rôles de l'agent.
@@ -140,7 +140,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
140140
@IsString({ each: true })
141141
@IsOptional()
142142
@ApiProperty()
143-
public roles?: string[]
143+
public roles?: string[];
144144

145145
/**
146146
* Configuration de sécurité de l'agent.
@@ -151,7 +151,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
151151
@ValidateNested()
152152
@Type(() => SecurityPartDTO)
153153
@ApiProperty({ type: SecurityPartDTO })
154-
public security: SecurityPartDTO
154+
public security: SecurityPartDTO;
155155

156156
/**
157157
* Indicateur de visibilité de l'agent.
@@ -163,7 +163,7 @@ export class AgentsCreateDto extends CustomFieldsDto {
163163
@IsBoolean()
164164
@IsOptional()
165165
@ApiProperty()
166-
public hidden: boolean
166+
public hidden: boolean;
167167
}
168168

169169
/**
@@ -194,7 +194,7 @@ export class AgentsDto extends AgentsCreateDto {
194194
*/
195195
@IsMongoId()
196196
@ApiProperty({ type: String })
197-
public _id: string
197+
public _id: string;
198198
}
199199

200200
/**
@@ -225,4 +225,11 @@ export class AgentsDto extends AgentsCreateDto {
225225
* };
226226
* ```
227227
*/
228-
export class AgentsUpdateDto extends PartialType(AgentsCreateDto) { }
228+
export class AgentsUpdateDto extends PartialType(AgentsCreateDto) {}
229+
230+
export class AgentsSelfUpdateDto extends AgentsUpdateDto {
231+
@IsString()
232+
@IsOptional()
233+
@ApiProperty()
234+
public currentPassword?: string;
235+
}

0 commit comments

Comments
 (0)