Aller au contenu

Migration Auth Investisseurs vers Better-Auth

Etat des lieux (post-cleanup, juin 2026)

L'auth investisseur est entièrement sur Better Auth :

  • Login / signup email, OAuth social, reset password, change password, 2FA login → /api/auth/* (plugins emailOTP, twoFactor)
  • Sessions server-side (better_auth_session) — plus de JWT Passport investisseur
  • Mots de passe et comptes OAuth dans better_auth_account — colonnes legacy supprimées de customers (password, tokensValidAfter, *AccountId)
  • MFA post-login (email update, password update, retrait, etc.) → encore Redis via POST /auth/mfa/* (action login retirée)
  • JwtAuthGuard valide uniquement la session Better Auth (renommage du guard prévu dans une PR dédiée)

Better-auth est aussi utilisé pour le backoffice admin et le PDP (project owners) :

  • Deux instances sur la même stack Postgres : clientAuth (/api/auth/*, cookie better-auth.*) et adminAuth (/api/auth/admin/*, cookie bricks-admin.*) — cf. better-auth/README.md
  • Admin : rows better_auth_user avec role = 'admin', guard AdminAuthGuard, plus de table admins ni de POST /admins/sign-in — cf. admins.md
  • PDP : même instance clientAuth que les investisseurs (tables partagées, cookies séparés par app)
Archive — état pré-migration - Passport JWT investisseur + admin, HMAC-SHA256, OAuth Passport, MFA login Redis, password reset Redis, table `admins` - Dual-auth guard, jwtExchangePlugin, endpoints `/customers/sign-in`, `/customers/password-recovery`, etc.

Decisions architecturales

1. Instance clientAuth partagée (PDP + Investisseurs)

L'instance clientAuth sert les apps client (investisseur web/mobile + PDP). better_auth_user reste la table user partagée. Chaque investisseur aura :

  • 1 row dans better_auth_user (id, email, name, emailVerified — gere par better-auth)
  • 1 row dans customers liee par FK sur better_auth_user.id (donnees metier — gere par un hook)

Pas de cross-domain cookies pour le moment — chaque app a ses propres cookies de session. Le SSO cross-app sera ajoute plus tard si necessaire.

2. Entites metier creees a chaque session

A chaque creation de session (databaseHooks.session.create.after), les deux entites metier sont creees en parallele via un pattern findOrCreate idempotent :

  • Customer (table customers) — via findOrCreateCustomerForBetterAuthUser (check par betterAuthUserId, creation si absent)
  • ProjectFinancingRequestOwner (table project_financing_request_owner) — via ProjectFinancingRequestOwnerService.findOrCreate (INSERT ON CONFLICT DO NOTHING)

Cela garantit qu'un utilisateur a toujours les deux entites, quelle que soit l'app depuis laquelle il se connecte. En cas de crash entre la creation du better_auth_user et la creation du Customer, l'entite est auto-creee au prochain login.

Le champ registeredFrom ('financing' ou 'invest') est envoye par le client dans le body du signup. Il sert uniquement a conditionner l'envoi du mail de bienvenue investisseur (envoye si registeredFrom === 'invest' ET Customer vient d'etre cree).

3. Sessions server-side

On passe de JWT stateless a des sessions stockees en DB. Avantages : revocation instantanee, pas besoin de tokensValidAfter, meilleure securite. Pas de Redis secondaryStorage pour le moment — on commence simple avec la DB.

4. Migration progressive des mots de passe (HMAC-SHA256 → Argon2)

Custom password.hash et password.verify dans better-auth :

  • Hash : Argon2 via @node-rs/argon2
  • Verify :
  • Si le hash commence par $argon2 → verification Argon2 standard
  • Sinon → verification legacy HMAC-SHA256, puis rehash explicite en Argon2 + update en DB via BetterAuthRepository.updateRehashLegacyPassword

Better-auth ne rehash pas automatiquement apres un verify custom (verifie dans le code source de signInEmail). Le rehash est fait manuellement dans le callback verify.

Les mots de passe sont migres de customers.password vers better_auth_account.password (avec providerId = 'credential') via une migration Flyway. Au premier login post-migration, le hash HMAC-SHA256 est remplace par un hash Argon2.

5. ~~Dual-auth pendant la transition~~ (terminé)

Tous les clients prod (web, mobile, PDP invest) utilisent Better Auth ; le fallback JWT Passport a été retiré. Le nom JwtAuthGuard est conservé temporairement (PR renaming à part).

6. Email verification via emailOTP

Le plugin emailOTP de better-auth est utilise pour la verification d'email :

  • OTP 6 chiffres, expiration 5 minutes
  • Envoi via Customer.io (transactional email) — resolution du customerId par email
  • sendVerificationOnSignUp: false — la verification est declenchee explicitement cote front
  • overrideDefaultEmailVerification: true — remplace le flow par defaut de better-auth

7. ~~JWT exchange plugin (migration mobile)~~ (retiré)

Plugin supprimé avec les endpoints legacy — front-app et mobile utilisent Better Auth directement (sessions cookie / SecureStore). L'impersonification admin passe par ClientAuthImpersonationService (plugin admin impersonateUser), pas par un échange JWT.

8. Social providers

Les 4 providers OAuth sont configures sur l'instance better-auth avec accountLinking active :

  • Google, Facebook, LinkedIn, Apple
  • trustedProviders pour le linking automatique de comptes

La migration Flyway cree les rows better_auth_account correspondants pour chaque provider OAuth existant.

9. Admin auth (Better Auth, instance dédiée)

Migrée en mai 2026 (#5057 / backoffice #5300) — hors scope de cette PR investisseur, mais même stack :

  • Instance adminAuth : routes /api/auth/admin/*, cookie bricks-admin.session_token
  • Admins = better_auth_user avec role = 'admin' ; table admins supprimée
  • Guard AdminAuthGuard — session cookie uniquement, pas de fallback JWT
  • OAuth admin : Google only (backoffice web)

10. Impersonification admin (login as customer)

Les super admins peuvent se connecter au compte d'un investisseur via GET /administration/customers/:customerId/admin-login. L'endpoint n'emet plus de JWT : CustomerAdminService.generateAdminImpersonationUrl appelle ClientAuthImpersonationService.impersonateUserAsAdmin (plugin admin better-auth clientAuth.api.impersonateUser), relaie les headers Set-Cookie de la session impersonifiee, et retourne { url: frontUrl } (URL brute, sans suffixe ?token=). Slack + AdminAction en DB pour l'audit.

Apres la migration vers better-auth, le front-app n'utilise plus de JWT.

Impersonification admin : ClientAuthImpersonationService.impersonateUserAsAdmin cree une session client via le plugin admin Better Auth et retourne { url: frontUrl }. Pas de ?token= ni d'echange JWT cote front.


Plan de migration en 3 PRs

PR 1 : API + Web

Objectif : Migrer l'auth investisseur (backend + frontend web) vers better-auth.

Better-auth factory

projects/api/src/lib/better-auth/better-auth.factory.ts :

  • [x] Custom password.hash Argon2 + password.verify avec support HMAC-SHA256 legacy + rehash
  • [x] databaseHooks.session.create.after : findOrCreate Customer + ProjectOwner en parallele, envoi sendAccountCreatedEvent si registeredFrom === 'invest' et Customer cree, mise a jour lastLoginAt
  • [x] registeredFrom envoye par le client ('invest' ou 'financing'), valide par un z.enum sur l'additionalField
  • [x] Plugin emailOTP pour la verification d'email via Customer.io
  • [x] Social providers (Google, Facebook, LinkedIn, Apple) avec account linking
  • [x] Plugin admin(), UUIDv7, support cross-origin cookies dev

Migrations Flyway

  • [x] V202604121000 : colonne betterAuthUserId (FK → better_auth_user.id, nullable) sur customers + unique index partiel
  • [x] V202604121001 : migration de donnees idempotente (customers → better_auth_user + better_auth_account pour credential et OAuth, backfill registeredFrom = 'invest' pour les investisseurs migres)

Guard investisseur (Better Auth only)

  • [x] JwtAuthGuard : session Better Auth cookie uniquement (nom historique — renommage PR dédiée)
  • [x] Chargement Customer par betterAuthUserId après validation session

Infrastructure

  • [x] better-auth:check-and-migrate script + integration CI (dev + prod)
  • [x] BetterAuthRepository : findOrCreateCustomerForBetterAuthUser, findUserById, creation customer, rehash password, revocation sessions, anonymisation

Frontend web (front-app)

  • [x] Client better-auth (auth-client.ts)
  • [x] Login via authClient.signIn.email() + session cookie
  • [x] Signup via authClient.signUp.email() + flow email verification (emailOTP)
  • [x] Session : auth.slice migre de JWT/Redux vers session better-auth
  • [x] Social login : SocialButton migres vers authClient.signIn.social()
  • [x] Routes : AuthenticatedRoute / UnauthenticatedRoute adaptes au flow session
  • [x] 2FA : Security2FAForm / Security2FAModal adaptes au flow emailOTP
  • [x] Admin impersonification : session client via ClientAuthImpersonationService (plugin admin impersonateUser)

Changement notable : referral code au signup

Le signUp.email() de better-auth n'accepte pas de champs custom. Le referrerCode ne peut plus etre envoye atomiquement dans le body du signup.

Nouveau flow (deux temps) :

  1. Pendant le formulaire : l'utilisateur saisit son code parrain → validation via GET /customers/referral-code-exists → si valide, stockage dans localStorage (pending_referrer_code)
  2. Apres le signup better-auth : si un code est en localStorage, appel POST /referrals/referrer-code avec credentials: 'include' (authentifie par le cookie session fraichement cree) → suppression du localStorage

PR 2 : Mobile / RNW (React Native + Expo)

Objectif : Migrer l'auth investisseur sur l'app mobile vers better-auth. Le force-update sera declenche avec PR 3 (apres passage du mot de passe a 12 caracteres).

Mobile (merged — PR #5056)

  • [x] Installer better-auth + @better-auth/expo
  • [x] Migrer la logique d'auth des ecrans login / signup vers better-auth (UI inchangee) + adapter le flow referral code au signup (deux temps : validation puis POST apres signup, cf section "referral code au signup")
  • [x] Remplacer le stockage MMKV du token par useSession() (SecureStore on native, browser cookie on web)
  • [x] Integrer le flow email verification (emailOTP)
  • [x] Integrer les social providers
  • [x] Integrer le flow 2FA (twoFactor.sendOtp / verifyOtp)

Note: Mobile and web authenticate via Better Auth sessions. Le force-update mobile (12 caractères min) reste à déclencher — cf. PR 3 checklist. Voir AUTH_FLOWS.md dans le module auth mobile.


PR 3 : Reset password + password policy + force-update

Objectif : Renforcer la politique de mot de passe, migrer le reset password vers better-auth, et declencher le force-update mobile pour finaliser la migration auth.

Password policy + force-update

  • [x] MIN_PASSWORD_LENGTH deja passe a 12 dans password.zod.ts (PR #5367, single source of truth pour passwordLengthValidator_zod, passwordMinLengthRegex et passwordValidator) + mettre a jour les hints i18n mobile (signupWithCredentials.passwordHint, resetPassword.passwordHint) et la traduction web EN (translations-web/en.json passwordCharacters)
  • [ ] Declencher le force-update mobile pour forcer tous les utilisateurs sur la nouvelle version better-auth (bloque tant que le password a 12 caracteres n'est pas merge)

Reset password (backend + web + mobile)

  • [x] emailAndPassword.sendResetPassword via Customer.io dans la factory better-auth
  • [x] emailAndPassword.onPasswordReset pour invalidation tokens legacy
  • [x] Forgot/reset password web (front-app) via authClient.requestPasswordReset / authClient.resetPassword
  • [x] Forgot/reset password mobile via Better Auth (useRequestPasswordRecovery, useResetPassword)
  • [x] Change password mobile via Better Auth (useChangePassword)
  • [x] Suppression RTK legacy updateUserPassword sur front-app
  • [x] Endpoints legacy /customers/password-recovery* et PATCH /customers/password supprimés

Apres les 3 PRs : Cleanup (fait — PR #5514)

  • [x] Supprimer Passport/JWT investisseur, endpoints auth legacy, deps associées
  • [x] Supprimer tokensValidAfter, customers.password, customers.*AccountId (V202606181700)
  • [x] Supprimer le fallback legacy du guard
  • [x] Supprimer Redis MFA login (login action, POST /auth/mfa/login-confirm-code)
  • [x] Supprimer jwtExchangePlugin et RTK legacy password sur front-app
  • [ ] Renommer JwtAuthGuard (PR dédiée)

Risques et mitigations

Risque Mitigation
Deconnexion de tous les utilisateurs Migration progressive + rehash transparent ; force-update mobile pour les derniers clients legacy
Mots de passe HMAC-SHA256 incompatibles Custom password.verify avec rehash transparent vers Argon2 (PR 1)
OAuth callback URLs changent Configurer les deux URLs dans les consoles providers pendant la transition
Performance sessions DB vs JWT stateless Sessions en DB pour commencer, Redis secondaryStorage si besoin plus tard
Migration de donnees corrompue Script idempotent + dry-run en staging avant prod
Admin auth impactee Admin migré sur adminAuth (mai 2026) — cookie prefix distinct, pas de collision avec investisseur
Admin impersonification cassee Session client via ClientAuthImpersonationService + plugin admin impersonateUser
PDP auth cassee Tests de non-regression sur l'auth PDP apres chaque PR backend
Crash entre creation user et creation Customer findOrCreate idempotent a chaque session — auto-heal au prochain login
Migration mobile sans re-login Force-update mobile pour basculer les derniers clients legacy JWT

Hors scope (a traiter separement)

  • Migration du MFA legacy Redis post-login (2fa.controller.ts) — login 2FA déjà sur Better Auth
  • Biometrie mobile (expo-local-authentication)
  • Cross-domain cookies / SSO entre apps (a ajouter plus tard si necessaire)