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/*(pluginsemailOTP,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 decustomers(password,tokensValidAfter,*AccountId) - MFA post-login (email update, password update, retrait, etc.) → encore Redis via
POST /auth/mfa/*(actionloginretirée) JwtAuthGuardvalide 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/*, cookiebetter-auth.*) etadminAuth(/api/auth/admin/*, cookiebricks-admin.*) — cf.better-auth/README.md - Admin : rows
better_auth_useravecrole = 'admin', guardAdminAuthGuard, plus de tableadminsni dePOST /admins/sign-in— cf.admins.md - PDP : même instance
clientAuthque 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
customersliee par FK surbetter_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(tablecustomers) — viafindOrCreateCustomerForBetterAuthUser(check parbetterAuthUserId, creation si absent)ProjectFinancingRequestOwner(tableproject_financing_request_owner) — viaProjectFinancingRequestOwnerService.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
customerIdpar email sendVerificationOnSignUp: false— la verification est declenchee explicitement cote frontoverrideDefaultEmailVerification: 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
trustedProviderspour 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/*, cookiebricks-admin.session_token - Admins =
better_auth_useravecrole = 'admin'; tableadminssupprimé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.hashArgon2 +password.verifyavec support HMAC-SHA256 legacy + rehash - [x]
databaseHooks.session.create.after:findOrCreateCustomer + ProjectOwner en parallele, envoisendAccountCreatedEventsiregisteredFrom === 'invest'et Customer cree, mise a jourlastLoginAt - [x]
registeredFromenvoye par le client ('invest'ou'financing'), valide par unz.enumsur l'additionalField - [x] Plugin
emailOTPpour 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: colonnebetterAuthUserId(FK →better_auth_user.id, nullable) surcustomers+ unique index partiel - [x]
V202604121001: migration de donnees idempotente (customers →better_auth_user+better_auth_accountpour credential et OAuth, backfillregisteredFrom = '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
CustomerparbetterAuthUserIdaprès validation session
Infrastructure¶
- [x]
better-auth:check-and-migratescript + 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.slicemigre de JWT/Redux vers session better-auth - [x] Social login :
SocialButtonmigres versauthClient.signIn.social() - [x] Routes :
AuthenticatedRoute/UnauthenticatedRouteadaptes au flow session - [x] 2FA :
Security2FAForm/Security2FAModaladaptes au flow emailOTP - [x] Admin impersonification : session client via
ClientAuthImpersonationService(plugin adminimpersonateUser)
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) :
- Pendant le formulaire : l'utilisateur saisit son code parrain → validation via
GET /customers/referral-code-exists→ si valide, stockage danslocalStorage(pending_referrer_code) - Apres le signup better-auth : si un code est en localStorage, appel
POST /referrals/referrer-codeaveccredentials: '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.mddans 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_LENGTHdeja passe a 12 danspassword.zod.ts(PR #5367, single source of truth pourpasswordLengthValidator_zod,passwordMinLengthRegexetpasswordValidator) + mettre a jour les hints i18n mobile (signupWithCredentials.passwordHint,resetPassword.passwordHint) et la traduction web EN (translations-web/en.jsonpasswordCharacters) - [ ] 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.sendResetPasswordvia Customer.io dans la factory better-auth - [x]
emailAndPassword.onPasswordResetpour invalidation tokens legacy - [x] Forgot/reset password web (
front-app) viaauthClient.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
updateUserPasswordsur front-app - [x] Endpoints legacy
/customers/password-recovery*etPATCH /customers/passwordsupprimé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 (
loginaction,POST /auth/mfa/login-confirm-code) - [x] Supprimer
jwtExchangePluginet 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)