Aller au contenu

MFA (post-login, Redis)

Contrôleur HTTP du flux d'authentification à deux facteurs (2FA) par code à 6 chiffres envoyé par email pour les actions sensibles post-login : changement d'email, de mot de passe, activation/désactivation du MFA, achat carte cadeau, validation email, etc. Source : mfa.controller.ts.

Le 2FA au login ne passe pas par ce contrôleur ni par Redis — il est géré par le plugin Better Auth twoFactor (sendOtp / verifyOtp côté client). Voir auth.md.

Le code MFA fait 6 chiffres (MFA_CODE_LENGTH), expire selon emailCodeExpiresIn (env config) et est stocké dans Redis sous la clé mfa_verification_<customerId>_<action>_v2. Le rate-limit utilise un backoff exponentiel (30 s × 2^(tryCount-1), capé à 5 tentatives).

Actions MFA supportées via Redis : email-validation, withdraw, bank-account-creation, password-update, email-update, mfa-enabled-update, gift-card-purchase (cf. MFAVerificationAction).

L'action login est retirée : POST /auth/mfa/login-confirm-code n'existe plus.

PATCH /auth/mfa

Active ou désactive le MFA pour l'investisseur courant. Déclenche d'abord une vérification MFA (envoi d'un code par email) ; le frontend doit ensuite appeler POST /auth/mfa/confirm-code avec l'action mfa-enabled-update pour finaliser le changement.

Pré-conditions

  • Session investisseur Better Auth valide (JwtAuthGuard)
  • Rôle CUSTOMER (UserRoleGuard)
  • La nouvelle valeur enabled doit différer de l'état courant

Request body

{
  "enabled": true
}

Validé via patchMfaEnabledRequestBody (api-communication).

Response

200 OK (corps vide — le handler ne retourne rien) — un email contenant un code à 6 chiffres est envoyé via Customer.io. Le client doit ensuite confirmer via /auth/mfa/confirm-code.

Erreurs

Code Statut Cause
2fa.mfa-enabled.patch-is-same 400 enabled vaut déjà la valeur actuelle de customer.mfaEnabled

POST /auth/mfa/confirm-code

Confirme un code MFA pour une action authentifiée (post-login) : changement d'email, de mot de passe, activation/désactivation du MFA, achat de carte cadeau, etc. Selon l'action contenue dans le contexte de vérification, déclenche le handler associé (InvestorAccountService.passwordUpdateConfirmed, MFAPreAuthFlowService.generateVerificationToken, etc.).

Pré-conditions

  • Session investisseur Better Auth valide (JwtAuthGuard)
  • Rôle CUSTOMER (UserRoleGuard)
  • Un contexte MFA actif doit exister en Redis pour cette action (déclenché auparavant par PATCH /auth/mfa ou POST /auth/mfa/trigger-mfa)

Request body

{
  "action": "password-update",
  "code": "123456"
}

Validé via mfaVerificationRequestPayload (api-communication).

Response

Le retour dépend de l'action confirmée :

  • gift-card-purchase{ "token": "<uuid>" } (token one-shot consommable par le flux d'achat carte cadeau, expire selon businessRules.mfa.verificationTokenExpirationInMinutes)
  • mfa-enabled-update, email-update, password-update, email-validation → effet de bord côté serveur, pas de body de retour structuré
  • withdraw, bank-account-creation → no-op (TODO migration de l'ancien 2FA)

Erreurs

Code Statut Cause
2fa.verification-code.no-verification-context 400 Aucun code MFA actif pour cette action
2fa.verification-code.wrong-code 400 Code incorrect
2fa.verification-code.code-expired 400 Code expiré
2fa.login-legacy-removed 400 Action login retirée — utiliser Better Auth twoFactor

GET /auth/mfa/:email/action/:action/next-retry-date

Endpoint public. Retourne la prochaine date à laquelle le client peut redemander un code MFA pour cet email + action. Utilisé par le frontend pour afficher un compte à rebours (ex : "Renvoyer un code dans 30 s").

Path params

  • email — adresse email du compte
  • action — une des valeurs de MFAVerificationAction (hors login)

Validés via getMfaNextRetryDatePathParams (api-communication).

Response

{
  "nextRetryDate": "2026-05-07T16:30:30.000Z"
}

nextRetryDate est undefined si aucun compteur de tentatives n'existe (jamais déclenché ou compteur expiré).

POST /auth/mfa/resend-code

Endpoint public. Renvoie le code MFA courant par email (ou en regénère un si le précédent a expiré). Utilisé pour les flux post-login publics (ex. validation email pendant l'onboarding).

Request body

{
  "email": "user@example.com",
  "action": "email-validation"
}

Validé via postMfaResendCodeRequestBody (api-communication).

Comportement

  • Si le code en cache est encore valide : renvoie le même code par email
  • Si expiré : génère un nouveau code, le sauve en cache et l'envoie
  • Action login : rejetée (2fa.login-legacy-removed)

Erreurs

Code Statut Cause
2fa.no-customer-for-email 404 Aucun compte avec cet email
2fa.no-code-to-resend 400 Aucun contexte MFA actif pour cet email + action (rien à renvoyer)
2fa.too-many-attempts 403 Rate-limit atteint avant nextRetryDate
2fa.login-legacy-removed 400 Action login retirée

POST /auth/mfa/trigger-mfa

Déclenche manuellement l'envoi d'un code MFA pour l'investisseur courant. Utilisé par le client pour amorcer un flux MFA post-login (ex : avant un achat de carte cadeau qui requiert un token MFA).

Pré-conditions

  • Session investisseur Better Auth valide (JwtAuthGuard)
  • Rôle CUSTOMER (UserRoleGuard)

Request body

Discriminé sur action (mfaContext). Exemples :

{
  "action": "password-update",
  "newPasswordArgon2Hash": "<argon2>",
  "currentSessionId": "<optional>"
}
{ "action": "gift-card-purchase" }

Le mot de passe est stocké uniquement dans better_auth_account.password (Argon2) — le contexte password-update ne transporte que newPasswordArgon2Hash.

Response

201 Created (corps vide — le handler ne retourne rien) — un email contenant le code est envoyé via Customer.io. Si un code valide existe déjà avec le même contexte, il est ré-envoyé tel quel sans regénération.

Liens