Phase 2: changement d'email avec vérification de possession hors-bande (suite #145) #64

Open
opened 2026-07-04 12:12:03 +00:00 by momsse · 1 comment
Owner

Migré depuis viziertronic/octant#147 — ouvert le 2026-07-03 par @momsse.

Contexte

Suite de #63 (fermé par #146). La Phase 1 a fermé la faille de prise de compte sans dépendance mail : un changement d'email self-service n'est plus marqué « vérifié » et n'entre plus dans la surface de merge fédéré (read.user_email_lookup). La Phase 2 rétablit un vrai changement d'email vérifié via une preuve de possession hors-bande.

Objectif

Permettre à un utilisateur de changer son email et de le faire re-devenir une adresse vérifiée (donc ré-éligible au merge fédéré et à l'unicité forte), sur preuve de possession de la nouvelle adresse.

Périmètre

  • Introduire un port MailSender (service applicatif) + un adaptateur infra (choix de provider à trancher : SMTP / API transactionnelle).
  • requestEmailChange génère un token à usage unique, CSPRNG, TTL borné (Duration), stocké côté agrégat en hash uniquement (jamais en clair), envoyé hors-bande à la nouvelle adresse.
  • confirmEmailChange(token) : le payload porte le token, comparaison constant-time dans l'agrégat, usage unique + expiration. Émettre UserEmailVerifiedEvent uniquement sur cette preuve.
  • Sur vérification réussie, ré-insérer l'email vérifié dans read.user_email_lookup (le projecteur consomme UserEmailVerifiedEvent), rétablissant :
    • l'unicité forte de l'email vérifié,
    • le merge fédéré correct pour l'email post-changement (lève la limitation Phase 1 : « le nouvel email ne fusionne pas tant qu'il n'est pas vérifié »).
  • Signaux de sécurité : publier un signal sur token invalide/expiré et sur tentative vers une adresse déjà prise.

Dette technique adjacente (remontée en review de #146)

  • Extraire un hook post-decide / pre-append dans executeCommand (@octant/event-sourcing) pour que RequestEmailChangeUseCase cesse de réimplémenter le pipeline load→decide→append inline (le dedup doit s'intercaler entre decide et append). Réintègre le short-circuit decidedEvents.length === 0.

Limitations Phase 1 levées par cette issue

  • Deux comptes peuvent aujourd'hui partager le même email non vérifié (les emails non vérifiés ne sont pas dans la surface d'unicité) → l'unicité forte revient avec la vérification.
  • EmailAlreadyInUseError est en Phase 1 un garde UX best-effort (lecture d'une projection éventuellement cohérente) ; la vérification Phase 2 apporte la garantie d'unicité réelle sur les emails vérifiés.
> _Migré depuis [viziertronic/octant#147](https://github.com/viziertronic/octant/issues/147) — ouvert le 2026-07-03 par @momsse._ ## Contexte Suite de #63 (fermé par #146). La **Phase 1** a fermé la faille de prise de compte sans dépendance mail : un changement d'email self-service n'est plus marqué « vérifié » et n'entre plus dans la surface de merge fédéré (`read.user_email_lookup`). La **Phase 2** rétablit un vrai changement d'email *vérifié* via une preuve de possession hors-bande. ## Objectif Permettre à un utilisateur de changer son email et de le faire re-devenir une adresse **vérifiée** (donc ré-éligible au merge fédéré et à l'unicité forte), sur preuve de possession de la nouvelle adresse. ## Périmètre - [ ] Introduire un port `MailSender` (service applicatif) + un adaptateur infra (choix de provider à trancher : SMTP / API transactionnelle). - [ ] `requestEmailChange` génère un token à usage unique, **CSPRNG**, **TTL borné** (`Duration`), stocké côté agrégat en **hash uniquement** (jamais en clair), envoyé hors-bande à la **nouvelle** adresse. - [ ] `confirmEmailChange(token)` : le payload porte le token, comparaison **constant-time** dans l'agrégat, usage unique + expiration. Émettre `UserEmailVerifiedEvent` **uniquement** sur cette preuve. - [ ] Sur vérification réussie, ré-insérer l'email vérifié dans `read.user_email_lookup` (le projecteur consomme `UserEmailVerifiedEvent`), rétablissant : - l'unicité forte de l'email vérifié, - le merge fédéré correct pour l'email post-changement (lève la limitation Phase 1 : « le nouvel email ne fusionne pas tant qu'il n'est pas vérifié »). - [ ] Signaux de sécurité : publier un signal sur token invalide/expiré et sur tentative vers une adresse déjà prise. ## Dette technique adjacente (remontée en review de #146) - [ ] Extraire un hook `post-decide / pre-append` dans `executeCommand` (`@octant/event-sourcing`) pour que `RequestEmailChangeUseCase` cesse de réimplémenter le pipeline load→decide→append inline (le dedup doit s'intercaler entre `decide` et `append`). Réintègre le short-circuit `decidedEvents.length === 0`. ## Limitations Phase 1 levées par cette issue - Deux comptes peuvent aujourd'hui partager le même email **non vérifié** (les emails non vérifiés ne sont pas dans la surface d'unicité) → l'unicité forte revient avec la vérification. - `EmailAlreadyInUseError` est en Phase 1 un garde UX best-effort (lecture d'une projection éventuellement cohérente) ; la vérification Phase 2 apporte la garantie d'unicité réelle sur les emails vérifiés.
Author
Owner

@momsse — 2026-07-03 (commentaire migré) :

Ajout au périmètre (remonté en review de #146) : normalisation de casse des emails. findUserByEmail / EmailAlreadyInUseError comparent aujourd'hui les emails comme des chaînes opaques, sans lowercasing. Inoffensif en Phase 1 (les emails non vérifiés n'entrent pas dans la surface de merge), mais dès que la Phase 2 réinsère les emails vérifiés dans read.user_email_lookup, des variantes de casse (Victim@Corp.com vs victim@corp.com) produiraient deux lignes pour une même boîte. Normaliser (au minimum lowercasing) avant insertion/lookup.

> _@momsse — 2026-07-03 (commentaire migré) :_ Ajout au périmètre (remonté en review de #146) : **normalisation de casse des emails**. `findUserByEmail` / `EmailAlreadyInUseError` comparent aujourd'hui les emails comme des chaînes opaques, sans lowercasing. Inoffensif en Phase 1 (les emails non vérifiés n'entrent pas dans la surface de merge), mais dès que la Phase 2 réinsère les emails vérifiés dans `read.user_email_lookup`, des variantes de casse (`Victim@Corp.com` vs `victim@corp.com`) produiraient deux lignes pour une même boîte. Normaliser (au minimum lowercasing) avant insertion/lookup.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
momsse/octant#64
No description provided.