Skip to content

Commit 007d6cc

Browse files
committed
feat: enhance MFA management and user experience
- Added a new command to clear MFA settings for agents, allowing for easier management of multi-factor authentication. - Updated the AuthController to include availability checks for TOTP and WebAuthn methods in the response. - Improved the login component to dynamically display the appropriate instructions based on available MFA methods. - Refactored the HTTP client plugin to prefer TOTP when both TOTP and WebAuthn are available, enhancing user flow. - Adjusted the auth module to ensure proper module referencing for circular dependencies.
1 parent 0fa0bb0 commit 007d6cc

7 files changed

Lines changed: 239 additions & 50 deletions

File tree

apps/api/nest-cli.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
"typeCheck": false,
1010
"watchAssets": true
1111
}
12-
}
12+
}

apps/api/src/core/agents/agents.command.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
import { Logger } from '@nestjs/common'
22
import { ModuleRef } from '@nestjs/core'
3+
import { Types } from 'mongoose'
34
import { Command, CommandRunner, InquirerService, Question, QuestionSet, SubCommand } from 'nest-commander'
4-
import { AgentsCreateDto } from '~/core/agents/_dto/agents.dto'
5+
import { AgentsCreateDto, AgentsUpdateDto } from '~/core/agents/_dto/agents.dto'
56
import { AgentsService } from '~/core/agents/agents.service'
67
import { Agents } from './_schemas/agents.schema'
78

9+
function isTotpEnabledForAgentSecurity(security: Record<string, unknown>): boolean {
10+
const otpKey = `${security.otpKey || ''}`.trim().replace(/\s+/g, '').toUpperCase()
11+
if (!otpKey) return false
12+
return /^[A-Z2-7]+=*$/.test(otpKey) && otpKey.length >= 16
13+
}
14+
15+
function hasWebAuthnKeyForAgentSecurity(security: Record<string, unknown>): boolean {
16+
const keys = security.u2fKey
17+
if (!Array.isArray(keys)) return false
18+
return keys.some((key) => {
19+
const entry = key && typeof key === 'object' ? (key as Record<string, unknown>) : {}
20+
return `${entry.credentialId || ''}`.trim() && `${entry.publicKey || ''}`.trim()
21+
})
22+
}
23+
824
/**
925
* Ensemble de questions interactives pour la création d'un agent.
1026
*
@@ -137,6 +153,74 @@ export class AgentsCreateCommand extends CommandRunner {
137153
}
138154
}
139155

156+
/**
157+
* Supprime le MFA (TOTP + clés FIDO/WebAuthn) d'un agent.
158+
*
159+
* @example
160+
* ```bash
161+
* yarn console agents clear-mfa admin
162+
* ```
163+
*/
164+
@SubCommand({ name: 'clear-mfa', arguments: '<username>' })
165+
export class AgentsClearMfaCommand extends CommandRunner {
166+
private readonly logger = new Logger(AgentsClearMfaCommand.name)
167+
168+
public constructor(
169+
protected moduleRef: ModuleRef,
170+
private readonly agentsService: AgentsService,
171+
) {
172+
super()
173+
}
174+
175+
async run(inputs: string[], _options: unknown): Promise<void> {
176+
const username = `${inputs[0] || ''}`.trim()
177+
if (!username) {
178+
console.error('Usage: yarn console agents clear-mfa <username>')
179+
return
180+
}
181+
182+
this.logger.log(`Clearing MFA for agent "${username}"...`)
183+
184+
try {
185+
const agent = (await this.agentsService.findOne<Agents>({ username })) as Agents | null
186+
if (!agent?._id) {
187+
console.error(`Agent introuvable: ${username}`)
188+
return
189+
}
190+
191+
const currentSecurity =
192+
agent.security && typeof agent.security === 'object'
193+
? typeof (agent.security as { toObject?: () => Record<string, unknown> }).toObject === 'function'
194+
? (agent.security as { toObject: () => Record<string, unknown> }).toObject()
195+
: { ...(agent.security as unknown as Record<string, unknown>) }
196+
: {}
197+
198+
const hadTotp = isTotpEnabledForAgentSecurity(currentSecurity)
199+
const hadWebAuthn = hasWebAuthnKeyForAgentSecurity(currentSecurity)
200+
const fidoKeyCount = Array.isArray(currentSecurity.u2fKey) ? currentSecurity.u2fKey.length : 0
201+
202+
if (!hadTotp && !hadWebAuthn) {
203+
console.log(`Aucun MFA actif pour "${username}".`)
204+
return
205+
}
206+
207+
await this.agentsService.update(agent._id as Types.ObjectId, {
208+
security: {
209+
...currentSecurity,
210+
otpKey: '',
211+
u2fKey: [],
212+
},
213+
} as AgentsUpdateDto)
214+
215+
console.log(`MFA désactivé pour "${username}".`)
216+
if (hadTotp) console.log('- TOTP supprimé')
217+
if (hadWebAuthn) console.log(`- ${fidoKeyCount} clé(s) FIDO/WebAuthn supprimée(s)`)
218+
} catch (error) {
219+
console.error('Erreur lors de la suppression du MFA', error)
220+
}
221+
}
222+
}
223+
140224
@SubCommand({ name: 'list' })
141225
export class AgentsListCommand extends CommandRunner {
142226
private readonly logger = new Logger(AgentsListCommand.name)
@@ -176,9 +260,14 @@ export class AgentsListCommand extends CommandRunner {
176260
* ```bash
177261
* yarn run console agents create
178262
* yarn run console agents list
263+
* yarn console agents clear-mfa <username>
179264
* ```
180265
*/
181-
@Command({ name: 'agents', arguments: '<task>', subCommands: [AgentsCreateCommand, AgentsListCommand] })
266+
@Command({
267+
name: 'agents',
268+
arguments: '<task>',
269+
subCommands: [AgentsCreateCommand, AgentsListCommand, AgentsClearMfaCommand],
270+
})
182271
export class AgentsCommand extends CommandRunner {
183272
/**
184273
* Constructeur de la commande agents

apps/api/src/core/auth/auth.controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ export class AuthController extends AbstractController {
291291
return res.status(HttpStatus.OK).json({
292292
requires2fa: true,
293293
challengeToken,
294+
totpAvailable: totpEnabled,
295+
webAuthnAvailable,
294296
method: webAuthnAvailable ? 'webauthn' : 'totp',
295297
methods: [webAuthnAvailable ? 'webauthn' : null, totpEnabled ? 'totp' : null].filter(Boolean),
296298
});
@@ -379,6 +381,8 @@ export class AuthController extends AbstractController {
379381
return res.status(HttpStatus.OK).json({
380382
requires2fa: true,
381383
challengeToken: preflight.challengeToken,
384+
totpAvailable: preflight.totpAvailable,
385+
webAuthnAvailable: preflight.webAuthnAvailable,
382386
method: preflight.webAuthnAvailable ? 'webauthn' : 'totp',
383387
methods: [preflight.webAuthnAvailable ? 'webauthn' : null, preflight.totpAvailable ? 'totp' : null].filter(
384388
Boolean,

apps/api/src/core/auth/auth.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { AuditsModule } from '../audits/audits.module';
2727
...configService.get<JwtModuleOptions>('jwt.options', {}),
2828
}),
2929
}),
30-
AgentsModule,
30+
forwardRef(() => AgentsModule),
3131
RolesModule,
3232
AuditsModule,
3333
forwardRef(() => KeyringsModule),

apps/api/src/instrument.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { nodeProfilingIntegration } from '@sentry/profiling-node'
1616
* Si SESAME_SENTRY_DSN n'est pas défini, Sentry reste désactivé et un avertissement est émis.
1717
*/
1818
if (!process.env.SESAME_SENTRY_DSN) {
19-
Logger.warn('SENTRY DSN not provided, Sentry is disabled', Sentry.constructor.name)
19+
Logger.warn('SENTRY DSN not provided, Sentry is disabled', 'Sentry')
2020
} else {
2121
Sentry.init({
2222
/** DSN de connexion à Sentry */
@@ -70,5 +70,5 @@ if (!process.env.SESAME_SENTRY_DSN) {
7070
Sentry.fsIntegration(),
7171
],
7272
})
73-
Logger.log(`Sentry initialized successfully`, Sentry.constructor.name)
73+
Logger.log('Sentry initialized successfully', 'Sentry')
7474
}

0 commit comments

Comments
 (0)