Lier un compte Entra ne rafraîchit jamais le profil : activer updateUserInfoOnLink (le rôle est déjà protégé) #113

Closed
opened 2026-10-06 08:36:29 +00:00 by nicolas-fryder · 1 comment

Ce qui manque

creerAuth fixe :

account: {
  accountLinking: { enabled: true, trustedProviders: ['microsoft'] },
},

Le lien se fait, mais le profil n'est jamais repris de l'IdP. name et image restent ceux que le compte portait à sa création — venus d'un import, d'une saisie d'administration, ou de ce que l'ancien système stockait.

Conséquence concrète : quelqu'un qui change de nom dans Entra — mariage, correction d'une faute de saisie, passage de « jdupont » à « Jeanne Dupont » — garde l'ancien partout dans la flotte, indéfiniment. Il n'existe aucun geste pour le corriger, sauf écrire en base.

better-auth a l'option, et elle est à false par défaut :

async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
  if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return
  …
}

Ce que ça NE toucherait pas, et je l'ai vérifié

La crainte évidente est qu'un rafraîchissement réécrive le role à chaque connexion — donc rétrograde la personne, puisque mapProfileToUser le dérive des groupes et qu'un satellite sans entra.groupes obtiendrait SANS_ROLE.

Ça ne peut pas arriver. applyUpdateUserInfoOnLink passe par parseAdditionalUserInputFromProviderProfile, qui écarte explicitement les champs non saisissables :

function parseAdditionalUserInputFromProviderProfile(options, profile = {}, action) {
  const schema = getFields(options, "user", "input")
  const allowedProfileFields = Object.create(null)
  for (const key of Object.keys(profile)) {
    if (schema[key]?.input === false) continue   // ← ici
    allowedProfileFields[key] = profile[key]
  }
  …
}

Et creerAuth déclare justement :

user: { additionalFields: { role: { type: 'string', input: false, defaultValue: SANS_ROLE } } },

Le role est donc sauté. Seuls name et image partent à l'updateUser. La politique « l'IdP dit QUI, l'application dit QUOI » tient, et elle tient par construction — pas par précaution.

La proposition

 account: {
-  accountLinking: { enabled: true, trustedProviders: ['microsoft'] },
+  // Le profil suit l'annuaire : un changement de nom dans Entra se propage à la
+  // connexion suivante. Le `role`, lui, est déclaré `input: false` et donc sauté
+  // par `parseAdditionalUserInputFromProviderProfile` : l'IdP dit QUI, jamais QUOI.
+  accountLinking: { enabled: true, trustedProviders: ['microsoft'], updateUserInfoOnLink: true },
 },

Une ligne, et le commentaire vaut autant que le changement — c'est lui qui empêchera quelqu'un de craindre la rétrogradation et de le retirer.

Si vous préférez laisser le choix au satellite, exposer accountLinking dans OptionsAuth marcherait aussi. Mais c'est une politique d'identité de flotte (D-004), et une flotte où la moitié des applications rafraîchit le nom et l'autre non est pire que les deux comportements.

D'où vient la demande

Nuage, rétrofitté la semaine dernière. Le besoin formulé par Nicolas : « à la connexion avec Entra on veut qu'ils soient liés aux comptes et qu'on récupère les infos comme le nom. Mais pas de changement de rôle, c'est le site qui définit les rôles. »

La première moitié ne marche pas aujourd'hui ; la seconde est déjà garantie.

Socle 0.14.0, better-auth 1.7.6.

## Ce qui manque `creerAuth` fixe : ```ts account: { accountLinking: { enabled: true, trustedProviders: ['microsoft'] }, }, ``` Le lien se fait, mais **le profil n'est jamais repris de l'IdP**. `name` et `image` restent ceux que le compte portait à sa création — venus d'un import, d'une saisie d'administration, ou de ce que l'ancien système stockait. Conséquence concrète : quelqu'un qui change de nom dans Entra — mariage, correction d'une faute de saisie, passage de « jdupont » à « Jeanne Dupont » — garde l'ancien partout dans la flotte, indéfiniment. Il n'existe aucun geste pour le corriger, sauf écrire en base. better-auth a l'option, et elle est à `false` par défaut : ```js async function applyUpdateUserInfoOnLink(c, userId, userInfo) { if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return … } ``` ## Ce que ça NE toucherait pas, et je l'ai vérifié La crainte évidente est qu'un rafraîchissement réécrive le `role` à chaque connexion — donc rétrograde la personne, puisque `mapProfileToUser` le dérive des groupes et qu'un satellite sans `entra.groupes` obtiendrait `SANS_ROLE`. **Ça ne peut pas arriver.** `applyUpdateUserInfoOnLink` passe par `parseAdditionalUserInputFromProviderProfile`, qui écarte explicitement les champs non saisissables : ```js function parseAdditionalUserInputFromProviderProfile(options, profile = {}, action) { const schema = getFields(options, "user", "input") const allowedProfileFields = Object.create(null) for (const key of Object.keys(profile)) { if (schema[key]?.input === false) continue // ← ici allowedProfileFields[key] = profile[key] } … } ``` Et `creerAuth` déclare justement : ```ts user: { additionalFields: { role: { type: 'string', input: false, defaultValue: SANS_ROLE } } }, ``` Le `role` est donc sauté. Seuls `name` et `image` partent à l'`updateUser`. La politique « l'IdP dit QUI, l'application dit QUOI » tient, et elle tient *par construction* — pas par précaution. ## La proposition ```diff account: { - accountLinking: { enabled: true, trustedProviders: ['microsoft'] }, + // Le profil suit l'annuaire : un changement de nom dans Entra se propage à la + // connexion suivante. Le `role`, lui, est déclaré `input: false` et donc sauté + // par `parseAdditionalUserInputFromProviderProfile` : l'IdP dit QUI, jamais QUOI. + accountLinking: { enabled: true, trustedProviders: ['microsoft'], updateUserInfoOnLink: true }, }, ``` Une ligne, et le commentaire vaut autant que le changement — c'est lui qui empêchera quelqu'un de craindre la rétrogradation et de le retirer. Si vous préférez laisser le choix au satellite, exposer `accountLinking` dans `OptionsAuth` marcherait aussi. Mais c'est une politique d'identité de flotte (D-004), et une flotte où la moitié des applications rafraîchit le nom et l'autre non est pire que les deux comportements. ## D'où vient la demande Nuage, rétrofitté la semaine dernière. Le besoin formulé par Nicolas : « à la connexion avec Entra on veut qu'ils soient liés aux comptes et qu'on récupère les infos comme le nom. Mais pas de changement de rôle, c'est le site qui définit les rôles. » La première moitié ne marche pas aujourd'hui ; la seconde est déjà garantie. Socle 0.14.0, better-auth 1.7.6.
Author
Owner

Corrigé dans 243cf81 (PR #114), mais pas avec l'option proposée. La mesure l'a écartée.

updateUserInfoOnLink ne règle pas le cas décrit. Rejoué sur le rappel OAuth réel (sign-in/social puis callback/microsoft, locataire simulé, better-auth 1.7.3) : applyUpdateUserInfoOnLink n'est appelé que dans la branche où le compte Microsoft n'est pas encore lié. Au premier lien, le nom est repris ; si la personne change de nom dans Entra ensuite, la connexion suivante passe par la branche « compte déjà lié » et ne touche plus au profil. Le test le montre : avec cette seule option, « Jeanne Dupont » reste « Jeanne Dupont » quand Entra dit « Jeanne Martin ».

Retenu : overrideUserInfoOnSignIn: true sur le fournisseur Microsoft. Il reprend le profil à chaque connexion, premier lien compris. Il passe par le même parseAdditionalUserInputFromProviderProfile, donc le role déclaré input: false est sauté, comme tu l'avais vérifié. En plus, mapProfileToUser ne rend plus de role du tout : le rôle est hors d'atteinte par construction.

À confirmer (D-052, marquée « recommandée ») : cette option reprend aussi l'adresse de l'annuaire, pas seulement le nom et la photo. C'est cohérent avec « l'IdP dit QUI », mais c'est un changement de plus que ce que demandait le ticket.

Trouvé en mesurant, et plus grave. Le filtre que tu citais saute un champ input: false aussi à la création. Or mapProfileToUser y mettait le rôle dérivé de entra.groupes. Résultat : provision: 'jit' créait chaque compte Entra sans rôle, quoi que dise roles.yaml. Aucun test ne le voyait, parce qu'ils appelaient la fonction de correspondance et pas le rappel. Désormais, le rôle des groupes est mis de côté puis posé par le hook de création, seulement quand la création vient du rappel OAuth. Chasse 25 dans reference/chasses.md.

test/entra-rappel.test.ts : 3 cas (jit avec groupe, jit sans groupe, compte créé par l'administration puis lié et renommé). Les trois rougissent quand on retire le correctif.

Corrigé dans 243cf81 (PR #114), mais pas avec l'option proposée. La mesure l'a écartée. **`updateUserInfoOnLink` ne règle pas le cas décrit.** Rejoué sur le rappel OAuth réel (`sign-in/social` puis `callback/microsoft`, locataire simulé, better-auth 1.7.3) : `applyUpdateUserInfoOnLink` n'est appelé que dans la branche où le compte Microsoft n'est **pas encore lié**. Au premier lien, le nom est repris ; si la personne change de nom dans Entra ensuite, la connexion suivante passe par la branche « compte déjà lié » et ne touche plus au profil. Le test le montre : avec cette seule option, « Jeanne Dupont » reste « Jeanne Dupont » quand Entra dit « Jeanne Martin ». **Retenu : `overrideUserInfoOnSignIn: true` sur le fournisseur Microsoft.** Il reprend le profil à chaque connexion, premier lien compris. Il passe par le même `parseAdditionalUserInputFromProviderProfile`, donc le `role` déclaré `input: false` est sauté, comme tu l'avais vérifié. En plus, `mapProfileToUser` ne rend plus de `role` du tout : le rôle est hors d'atteinte par construction. À confirmer (D-052, marquée « recommandée ») : cette option reprend aussi **l'adresse** de l'annuaire, pas seulement le nom et la photo. C'est cohérent avec « l'IdP dit QUI », mais c'est un changement de plus que ce que demandait le ticket. **Trouvé en mesurant, et plus grave.** Le filtre que tu citais saute un champ `input: false` aussi à la **création**. Or `mapProfileToUser` y mettait le rôle dérivé de `entra.groupes`. Résultat : `provision: 'jit'` créait **chaque** compte Entra sans rôle, quoi que dise `roles.yaml`. Aucun test ne le voyait, parce qu'ils appelaient la fonction de correspondance et pas le rappel. Désormais, le rôle des groupes est mis de côté puis posé par le hook de création, seulement quand la création vient du rappel OAuth. Chasse 25 dans `reference/chasses.md`. `test/entra-rappel.test.ts` : 3 cas (jit avec groupe, jit sans groupe, compte créé par l'administration puis lié et renommé). Les trois rougissent quand on retire le correctif.
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
Infrastructure/eqs-platform#113
No description provided.