<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
  <title>Carnet — Fabien Dorey</title>
  <link>https://fabiendorey.fr/carnet/</link>
  <description>Des notes courtes, écrites sur le terrain : faire évoluer du code qui a de l’histoire, livrer sous contrainte, travailler avec l’IA. Sans prétention, avec des exemples.</description>
  <language>fr</language>
  <atom:link href="https://fabiendorey.fr/carnet/rss.xml" rel="self" type="application/rss+xml"/>
  <lastBuildDate>Wed, 11 Nov 2026 07:00:00 GMT</lastBuildDate>
  <item>
    <title>Avant de lui confier un bug, je lui ai appris où chercher</title>
    <link>https://fabiendorey.fr/carnet/agent-de-code-contexte/</link>
    <guid isPermaLink="true">https://fabiendorey.fr/carnet/agent-de-code-contexte/</guid>
    <pubDate>Wed, 11 Nov 2026 07:00:00 GMT</pubDate>
    <description>Deux semaines avec Claude Code sur un back-office qui a de l’histoire. Ce que je lui ai donné avant de lui demander quoi que ce soit, un bug d’export qu’il a démêlé grâce à ça, et l’endroit où je mets la limite.</description>
    <content:encoded><![CDATA[<p>Cela fait deux semaines que nous utilisons Claude Code sur notre back-office de prise de commande. Deux semaines, ce n’est pas un bilan : c’est un début, et je l’écris comme tel.</p>
<p>Mais ces deux semaines m’ont déjà appris une chose. Sur une application qui a de l’histoire, <strong>ce qu’il faut savoir pour corriger un bug n’est presque jamais dans le code</strong>. C’est dans le plan batch, dans les logs d’une tâche automatisée, dans une table, dans la page qui décrit ce qu’une autre application attend de la nôtre. Un agent qui ne voit que le dépôt devine. Je voulais un agent qui aille vérifier.</p>
<p>Alors avant de lui confier quoi que ce soit, j’ai passé du temps à lui apprendre où chercher.</p>
<h2>Trois étages</h2>
<p>Rien d’exotique : des fichiers texte et quelques gestes rangés.</p>
<ul>
<li><strong>Un <code>CLAUDE.md</code>, partagé par l’équipe.</strong> L’architecture, les conventions du projet, et surtout les liens vers les pages de flux de notre Confluence, dont il garde le contenu sous la main : ce que l’application reçoit, ce qu’elle envoie, à qui, et qui dépend d’elle.</li>
<li><strong>Un <code>CLAUDE.local.md</code>, propre à mon poste.</strong> Comment se brancher aux bases et aux machines depuis mon poste. Hors production, sans exception.</li>
<li><strong>Des skills, pour les gestes qui reviennent.</strong> Retrouver un flux et ce qui l’entoure. Préparer une livraison, scripts de migration compris — ceux de la <a href="/carnet/flyway-migrations-sql/">note précédente</a>.</li>
</ul>
<p>Il sait aussi comment tourne la maison : notre back-office suit un plan batch précis, marché par marché, et il sait où retrouver les logs de chaque tâche automatisée.</p>
<p>Le résultat, c’est qu’il ne part plus d’une page blanche. Face à un bug, <strong>il se fabrique son contexte lui-même</strong> : il lit le plan, ouvre les logs, regarde en base, relit le flux concerné. Exactement ce que je ferais, dans le même ordre.</p>
<h2>Un bug, pour voir</h2>
<p>Le métier nous remonte un problème : aucun message n’est généré pour le service d’éditique, c’est-à-dire l’imprimeur.</p>
<p>Le contexte tient en trois lignes. Nos bases sont multi-pays. L’export vers l’éditique est un job qui ramasse en base les messages disponibles et en fait un fichier ; il est lancé une fois par marché. Pendant longtemps il n’y a eu qu’un marché. Nous venions d’en ajouter deux.</p>
<p>Trois marchés, donc trois exécutions, à cinq minutes d’intervalle :</p>
<div class="code"><div class="code__label">text</div><pre tabindex="0"><code>T          export --market=A   -&gt; picks up every pending message, flags them all as A
T + 5 min  export --market=B   -&gt; nothing left to export
T + 10 min export --market=C   -&gt; nothing left to export</code></pre></div>
<p>Le paramètre qui indique le marché existait bien. Il ne servait qu’aux logs. Il ne filtrait rien.</p>
<div class="code"><div class="code__label">Java</div><pre tabindex="0"><code>// Simplified. Before: the market only reached the logs.
log.info(&quot;Export started for market {}&quot;, market);
List&lt;Message&gt; pending = messageRepository.findPending();

// After: the market decides what the job is allowed to take.
List&lt;Message&gt; pending = messageRepository.findPendingByMarket(market);</code></pre></div>
<p>Tant qu’il n’y avait qu’un marché, ce code était juste. Il l’est resté longtemps. Le jour où deux pays sont arrivés, la première exécution a tout pris, tout étiqueté à son nom, et les deux suivantes ont tourné à vide. C’est le genre de bug que le code hérité fabrique tout seul : rien n’a cassé, c’est le monde autour qui a changé.</p>
<p>Ce que Claude Code m’a rendu, ce n’est pas seulement la ligne à corriger. C’est <strong>l’histoire complète</strong> : l’historique du plan batch et de ses trois exécutions, ce qui était disponible comme messages à ce moment-là, la cause, le correctif proposé, et un plan de rattrapage pour les messages étiquetés au mauvais marché.</p>
<p>Le correctif tient en une ligne. Tout le reste est du contexte, et c’est ce reste qui prend du temps quand on le reconstitue à la main.</p>
<h2>Où je mets la limite</h2>
<p>Il propose. La décision reste la mienne, et certaines portes restent fermées.</p>
<p><strong>Pas de production.</strong> Ses accès s’arrêtent aux autres environnements. Ce n’est pas un réglage que j’ajusterai quand j’aurai davantage confiance : c’est une porte qui ne s’ouvre pas.</p>
<p>Je lis en ce moment deux textes qui m’aident à mettre des mots là-dessus. Addy Osmani <a href="https://addyosmani.com/blog/coding-agents-manager/">décrit le développeur qui devient le manager de ses agents</a> : le plus difficile n’est plus d’écrire le code, c’est de bien confier le travail et de vérifier ce qui revient. Et <a href="https://x.com/hanakoxbt/status/2091515787366306154">un long texte sur les boucles et les graphes d’agents</a> défend une idée que je trouve très juste : on ouvre les vannes selon ce que coûte une erreur, pas selon l’assurance affichée par le modèle.</p>
<p>J’en garde trois règles, pour la suite :</p>
<ul>
<li><strong>Le contrôle d’abord.</strong> « Ça a l’air bon » n’est pas une vérification. Il faut quelque chose qui puisse échouer : un test, un cas rejoué sur une base de recette.</li>
<li><strong>La réversibilité décide.</strong> Une correction isolée se défait d’un revert. Une migration, une suppression, une écriture sur des données de production ne se défont pas : celles-là, il les prépare, quelqu’un les relit, quelqu’un d’autre les passe.</li>
<li><strong>Une cause confirmée devient une règle.</strong> Ce bug doit laisser une trace dans le <code>CLAUDE.md</code> : <em>un paramètre de job qui ne filtre rien est suspect</em>. Sinon je le recroiserai.</li>
</ul>
<h2>Ce que j’en retiens</h2>
<blockquote>
<p>Un agent vaut ce que vaut le contexte qu’on lui <em>laisse trouver</em>.</p>
</blockquote>
<p>Je n’ai pas gagné du temps parce qu’un modèle écrit du Java plus vite que moi. J’en ai gagné parce que j’ai écrit, une fois, ce que je sais de cette application et où se trouve le reste — ce que j’aurais dû écrire de toute façon, pour le prochain collègue qui arrive.</p>
<p>Deux semaines, donc. Trop tôt pour dire ce qui tiendra. Je reviendrai ici dans quelques mois, avec ce qui a raté.</p>
]]></content:encoded>
  </item>
  <item>
    <title>La base de données aussi se livre</title>
    <link>https://fabiendorey.fr/carnet/flyway-migrations-sql/</link>
    <guid isPermaLink="true">https://fabiendorey.fr/carnet/flyway-migrations-sql/</guid>
    <pubDate>Tue, 10 Nov 2026 07:00:00 GMT</pubDate>
    <description>Des modifications SQL passées à la main, jusqu’en production, sans que l’exploitation soit dans la boucle. Comment Flyway a remis le schéma de la base dans le même circuit que le code — et pourquoi c’est d’abord une affaire de transparence.</description>
    <content:encoded><![CDATA[<p>Sur l’application dont j’ai hérité, le code avait son circuit : un dépôt, des versions, des livraisons. La base de données, non. Les modifications de schéma étaient passées <strong>à la main, directement sur les environnements</strong>, production comprise. Une colonne ajoutée ici, un index là, un script gardé dans un coin ou retapé de mémoire.</p>
<p>Les mises en production fonctionnaient. Magiquement. Ou pas toujours. Et quand elles ne fonctionnaient pas, l’équipe d’exploitation découvrait le sujet en même temps que l’incident : elle n’avait pas été impliquée, puisqu’il n’y avait rien à lui confier.</p>
<p>Ce n’est pas un procès. C’est une habitude qui s’installe sans bruit dans une équipe qui connaît sa base par cœur, et qui tient tant que les mêmes personnes sont là. Mais elle a un coût, et il est toujours payé le jour de la mise en production.</p>
<h2>Ce que le SQL à la main coûte</h2>
<ul>
<li><strong>Personne ne sait ce qui a été livré.</strong> L’état de la base est la somme de gestes dont il ne reste aucune trace. La question « qu’est-ce qui a changé depuis la dernière fois ? » n’a pas de réponse.</li>
<li><strong>Les environnements dérivent.</strong> Un script passé en recette et oublié en production, un autre corrigé au vol sur un seul environnement : ce qu’on teste n’est plus ce qu’on livre.</li>
<li><strong>Le code et la base ne sont plus synchronisés.</strong> Une version de l’appli attend une colonne ; rien ne garantit qu’elle existe là où on la déploie.</li>
<li><strong>La production devient une affaire d’initiés.</strong> Ceux qui savent passent les scripts, les autres regardent. L’exploitation, qui porte pourtant la disponibilité, est tenue à l’écart de ce qui la menace le plus.</li>
</ul>
<h2>Le principe : un changement, un fichier</h2>
<p>Flyway est un outil de migration de base de données, et son idée est d’une simplicité désarmante. Chaque modification du schéma est un fichier SQL, numéroté, rangé dans le dépôt à côté du code :</p>
<div class="code"><div class="code__label">text</div><pre tabindex="0"><code>db/migration/
  V1__baseline.sql
  V2__add_delivery_note_to_orders.sql
  V3__create_order_history_table.sql</code></pre></div>
<div class="code"><div class="code__label">SQL</div><pre tabindex="0"><code>-- V2__add_delivery_note_to_orders.sql
ALTER TABLE orders ADD COLUMN IF NOT EXISTS delivery_note VARCHAR(255);

CREATE INDEX IF NOT EXISTS idx_orders_delivery_date ON orders (delivery_date);</code></pre></div>
<p>Dans chaque base, Flyway tient une table d’historique : quelles migrations ont été jouées, quand, par qui, avec quelle empreinte. À l’exécution, il compare les fichiers à cet historique et ne joue que ce qui manque, dans l’ordre.</p>
<figure class="schema">
<svg viewBox="0 0 342 170" role="img" aria-label="Les mêmes fichiers de migration, numérotés, sont joués dans le même ordre en développement, en recette puis en production">
<g fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round">
<rect pathLength="1" x="10" y="40" width="46" height="24" rx="6"/>
<rect pathLength="1" x="10" y="72" width="46" height="24" rx="6"/>
<rect pathLength="1" x="10" y="104" width="46" height="24" rx="6"/>
<path pathLength="1" d="M60 84 C 84 76, 100 92, 122 84"/>
<circle pathLength="1" cx="140" cy="84" r="17"/>
<path pathLength="1" d="M158 84 C 176 78, 188 90, 206 84"/>
<circle pathLength="1" cx="224" cy="84" r="17"/>
<path pathLength="1" d="M242 84 C 260 78, 272 90, 290 84"/>
<circle pathLength="1" cx="308" cy="84" r="17"/>
</g>
<circle class="pt" cx="140" cy="84" r="4"/><circle class="pt" cx="224" cy="84" r="4"/><circle class="pt" cx="308" cy="84" r="4"/>
<text x="33" y="56" text-anchor="middle">V1</text>
<text x="33" y="88" text-anchor="middle">V2</text>
<text x="33" y="120" text-anchor="middle">V3</text>
<text x="140" y="124" text-anchor="middle">Dev</text>
<text x="224" y="124" text-anchor="middle">Recette</text>
<text x="308" y="124" text-anchor="middle">Prod</text>
</svg>
<figcaption>Fig. 1 — Les mêmes fichiers, dans le même ordre, partout</figcaption>
</figure>

<p>La conséquence est immédiate : la production ne reçoit plus que des scripts qui ont déjà tourné, à l’identique, sur les environnements précédents. La mise en production cesse d’être une première fois.</p>
<h2>Partir d’une base qui existe déjà</h2>
<p>On ne met pas en place Flyway sur une page blanche. La base est là, avec son passé, et parfois avec des environnements qui ne se ressemblent plus tout à fait.</p>
<p>Le point de départ s’appelle une <em>baseline</em> : on déclare l’état actuel comme la version 1, sans rien rejouer, et tout ce qui suit passe par des migrations. C’est aussi le moment de comparer les schémas entre environnements et de résorber les écarts — un travail ingrat, mais qu’on ne fait qu’une fois.</p>
<h2>Les règles qui vont avec</h2>
<p>L’outil ne suffit pas. Ce qui change les choses, ce sont quelques règles tenues sans exception.</p>
<ul>
<li><strong>Plus aucun SQL de structure à la main.</strong> Si ce n’est pas dans une migration, ça n’existe pas. La règle vaut aussi pour les urgences — surtout pour les urgences.</li>
<li><strong>Une migration jouée ne se modifie plus.</strong> Flyway vérifie l’empreinte de chaque fichier : toucher à un script déjà appliqué bloque la suite. Pour corriger, on écrit la migration suivante.</li>
<li><strong>Les scripts sont relus comme du code.</strong> Même dépôt, même revue, même historique. Le changement de schéma voyage avec la version de l’appli qui en a besoin.</li>
<li><strong>Un script doit pouvoir être rejoué sans dégât.</strong> <code>IF NOT EXISTS</code>, <code>IF EXISTS</code> : une migration idempotente ne casse rien si elle retombe sur une base où le changement est déjà là — ce qui arrive forcément quand on part d’environnements modifiés à la main.</li>
<li><strong>On corrige vers l’avant.</strong> Pas de retour arrière improvisé en production : le plan de repli se prépare, s’écrit et se relit avant la livraison.</li>
</ul>
<h2>Des règles tenues par le pipeline</h2>
<p>Une règle qui repose sur la vigilance de chacun finit par céder, un vendredi soir. Celles-ci sont donc portées par GitLab, avec deux jobs.</p>
<p>Le premier est un <strong>contrôle</strong>, qui tourne à chaque modification : il vérifie que les scripts respectent les règles de nommage, et qu’ils sont idempotents. Un fichier mal nommé, Flyway l’ignore en silence ; un script non rejouable, on le découvre en production. Autant que ce soit le pipeline qui le dise, avant la relecture.</p>
<p>Le second est le <strong>job de migration</strong> : un <code>validate</code>, puis un <code>migrate</code>.</p>
<div class="code"><div class="code__label">YAML</div><pre tabindex="0"><code>stages: [check, migrate]

check-migrations:
  stage: check
  script:
    - ./ci/check-naming.sh db/migration       # V&lt;version&gt;__&lt;description&gt;.sql
    - ./ci/check-idempotence.sh db/migration  # safe to run twice

migrate:
  stage: migrate
  when: manual
  script:
    - flyway validate   # do the files still match what was applied?
    - flyway migrate    # apply pending migrations, in order</code></pre></div>
<p>Le <code>validate</code> refuse d’aller plus loin si un script déjà appliqué a été modifié ou s’il en manque un. Le <code>migrate</code> ne joue que ce qui reste à jouer.</p>
<h2>Rendre la production à ceux qui l’opèrent</h2>
<p>Le versioning n’était que la moitié du chantier. L’autre moitié, la plus utile, a été de former l’équipe d’exploitation à exécuter ces migrations elle-même.</p>
<p>Concrètement, c’est elle qui lance le job de migration. Avant de livrer, elle voit exactement ce qui va être joué : la liste des scripts est dans le dépôt, relue et contrôlée. Après, le journal du job et la table d’historique disent ce qui l’a été. Elle n’exécute plus un geste qu’on lui dicte : elle opère un outil qu’elle comprend, sur une liste qu’elle peut lire.</p>
<p>Et pour l’équipe de développement, c’est une discipline salutaire : un script que quelqu’un d’autre va passer s’écrit avec plus de soin qu’un script qu’on passe soi-même, tard, en se disant que ça ira.</p>
<h2>Un historique qui ne ment pas</h2>
<p>Je n’avais pas anticipé à quel point ce dossier de migrations deviendrait une documentation. Chaque fichier dit ce qui a changé, son nom dit pourquoi, et l’historique du dépôt dit qui, quand, et pour quelle livraison. La table d’historique, elle, dit ce qui tourne réellement sur chaque environnement.</p>
<p>Contrairement à un schéma dessiné dans un wiki, celui-là ne peut pas être en retard : c’est lui qui fait la base.</p>
<h2>Ce que j’en retiens</h2>
<blockquote>
<p>La production est l’affaire de <em>tous</em>.</p>
</blockquote>
<p>Pas celle de quelques-uns qui savent, ni celle d’une équipe qui subit. Livrer, c’est être transparent sur ce que l’on livre : pouvoir le montrer avant, le vérifier après, et le retrouver dans six mois.</p>
<p>Flyway n’a rien de spectaculaire. C’est un dossier de fichiers numérotés, une table et deux jobs. Mais c’est souvent ça, une bonne pratique : rendre visible ce qui se faisait dans l’ombre, et confier à chacun la part qui lui revient.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Broker ou multi-IdP, dans une iframe</title>
    <link>https://fabiendorey.fr/carnet/broker-ou-multi-idp/</link>
    <guid isPermaLink="true">https://fabiendorey.fr/carnet/broker-ou-multi-idp/</guid>
    <pubDate>Tue, 10 Nov 2026 07:00:00 GMT</pubDate>
    <description>Ouvrir une appli de service client aux agents d’un prestataire, quand elle vit dans une iframe et que deux fournisseurs d’identité cohabitent. Monter notre propre broker, ou laisser l’appli gérer les deux ? Retour sur un choix par défaut, mais assumé.</description>
    <content:encoded><![CDATA[<p>Le besoin de départ tient en une phrase : permettre aux agents d’un prestataire, dont les identités vivent hors du groupe, de s’authentifier dans notre application de service client. Ce prestataire gère ses comptes dans <strong>son propre Keycloak</strong>, managé par ses soins. Nos collaborateurs, eux, sont dans <strong>Entra ID</strong>. Deux fournisseurs d’identité, une seule appli.</p>
<p>Jusque-là, rien d’exotique. Ce qui a compliqué l’affaire, c’est l’endroit où vit l’application : elle n’est pas ouverte dans un onglet, elle est embarquée dans l’outil de support des agents, <strong>dans une iframe</strong>.</p>
<figure class="schema">
<svg viewBox="0 0 342 200" role="img" aria-label="Les collaborateurs passent par Entra ID, les agents du prestataire par Keycloak, et tous arrivent dans la même appli, elle-même embarquée dans l'outil de support">
<g fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round">
<path pathLength="1" d="M14 50 C 50 42, 96 58, 138 50"/>
<path pathLength="1" d="M14 150 C 50 158, 96 142, 138 150"/>
<circle pathLength="1" cx="156" cy="50" r="17"/>
<circle pathLength="1" cx="156" cy="150" r="17"/>
<path pathLength="1" d="M174 50 C 222 50, 226 100, 266 100"/>
<path pathLength="1" d="M174 150 C 222 150, 226 100, 266 100"/>
<rect pathLength="1" x="268" y="74" width="52" height="52" rx="10"/>
<rect pathLength="1" x="254" y="60" width="80" height="80" rx="14"/>
</g>
<circle class="pt" cx="14" cy="50" r="5"/><circle class="pt" cx="14" cy="150" r="5"/><circle class="pt" cx="156" cy="50" r="4"/><circle class="pt" cx="156" cy="150" r="4"/>
<text x="6" y="28">Collaborateurs</text>
<text x="6" y="128">Agents prestataire</text>
<text x="156" y="88" text-anchor="middle">Entra ID</text>
<text x="156" y="188" text-anchor="middle">Keycloak</text>
<text x="294" y="104" text-anchor="middle">Appli</text>
<text x="294" y="50" text-anchor="middle">Support</text>
</svg>
<figcaption>Fig. 1 — Deux émetteurs, une appli, et un cadre autour</figcaption>
</figure>

<h2>Ce que l’iframe casse</h2>
<p>Une connexion OIDC classique repose sur des redirections : l’appli envoie le navigateur chez le fournisseur d’identité, qui le renvoie une fois l’utilisateur authentifié. Dans une iframe, cette mécanique se grippe à deux endroits.</p>
<ul>
<li><strong>Les redirections sont bloquées.</strong> Les pages de connexion refusent d’être affichées dans un cadre — c’est une protection contre le détournement de clic, et elle est légitime. Impossible, donc, de dérouler le login à l’intérieur de l’iframe.</li>
<li><strong>Les cookies ne suivent plus.</strong> Vue du navigateur, notre appli est un contenu tiers dans la page de l’outil de support. Son cookie de session est un cookie tiers, et les navigateurs les bloquent de plus en plus.</li>
</ul>
<p>Restait une question très concrète, à laquelle nous n’avions pas de réponse toute faite : si le login se passe ailleurs, comment faire revenir le résultat jusqu’à l’iframe ?</p>
<h2>La cinématique retenue</h2>
<p>La réponse tient en trois pièces : une fenêtre à part, un message, un cookie partitionné.</p>
<figure class="schema">
<svg viewBox="0 0 342 220" role="img" aria-label="L'iframe ouvre une popup, la popup déroule le login chez le fournisseur d'identité, renvoie un code à usage unique à l'iframe, qui l'échange auprès du backend contre un cookie de session partitionné">
<g fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round">
<rect pathLength="1" x="10" y="30" width="84" height="50" rx="10"/>
<rect pathLength="1" x="150" y="30" width="84" height="50" rx="10"/>
<circle pathLength="1" cx="310" cy="55" r="17"/>
<rect pathLength="1" x="10" y="160" width="84" height="50" rx="10"/>
<path pathLength="1" d="M96 45 L146 45"/>
<path pathLength="1" d="M236 55 L291 55"/>
<path pathLength="1" d="M148 66 L98 66"/>
<path pathLength="1" d="M52 82 L52 156"/>
</g>
<circle class="pt" cx="146" cy="45" r="3.5"/><circle class="pt" cx="291" cy="55" r="3.5"/><circle class="pt" cx="98" cy="66" r="3.5"/><circle class="pt" cx="52" cy="156" r="3.5"/>
<text x="52" y="59" text-anchor="middle">Iframe</text>
<text x="192" y="59" text-anchor="middle">Popup</text>
<text x="310" y="92" text-anchor="middle">IdP</text>
<text x="52" y="189" text-anchor="middle">Backend</text>
<text x="121" y="38" text-anchor="middle">1</text>
<text x="263" y="48" text-anchor="middle">2</text>
<text x="123" y="82" text-anchor="middle">3</text>
<text x="62" y="123">4</text>
<text x="130" y="140">1 · Ouvre le login</text>
<text x="130" y="160">2 · S’authentifie</text>
<text x="130" y="180">3 · Code à usage unique</text>
<text x="130" y="200">4 · Cookie partitionné</text>
</svg>
<figcaption>Fig. 2 — Le login sort du cadre, la session y revient</figcaption>
</figure>

<p>L’iframe ouvre le login dans une <strong>popup</strong>. Dans cette fenêtre, plus de cadre : les redirections vers le fournisseur d’identité fonctionnent normalement, et le backend termine le flux OIDC comme il le ferait pour n’importe quelle appli web. La popup prévient ensuite l’iframe avec <code>postMessage</code>, puis se ferme.</p>
<div class="code"><div class="code__label">JavaScript</div><pre tabindex="0"><code>// Inside the iframe: open the login in a popup, then wait for its answer.
const popup = window.open(`/login/${provider}`, &quot;login&quot;, &quot;popup,width=480,height=640&quot;);

window.addEventListener(&quot;message&quot;, async (event) =&gt; {
  // only our own popup, on our own origin
  if (event.origin !== window.location.origin || event.source !== popup) return;

  // a one-time code travels between windows, never the tokens themselves
  await fetch(&quot;/session&quot;, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
    body: JSON.stringify({ code: event.data.code }),
  });
  startApp();
});</code></pre></div>
<p>Ce qui transite entre les deux fenêtres n’est pas un jeton, mais une preuve à usage unique et à durée de vie très courte. L’iframe l’échange auprès du backend, qui répond en posant le cookie de session — et c’est parce que ce cookie est posé <em>depuis l’iframe</em> qu’il peut être <strong>partitionné</strong> :</p>
<div class="code"><div class="code__label">http</div><pre tabindex="0"><code>Set-Cookie: __Host-session=…; Path=/; Secure; HttpOnly; SameSite=None; Partitioned</code></pre></div>
<p>Un cookie partitionné (CHIPS) n’existe que pour le couple « notre appli, embarquée dans cet outil de support ». Il ne permet aucun pistage d’un site à l’autre, ce qui lui vaut d’être accepté là où un cookie tiers classique est refusé. Les jetons, eux, ne quittent jamais le backend.</p>
<p>Trois précautions vont avec :</p>
<ul>
<li><strong>Vérifier l’origine</strong> de chaque message reçu, des deux côtés. Un <code>postMessage</code> sans contrôle d’origine est une porte ouverte.</li>
<li><strong>Limiter qui peut nous embarquer</strong> : l’appli n’accepte d’être affichée que dans l’outil de support (<code>frame-ancestors</code>), nulle part ailleurs.</li>
<li><strong>Protéger contre le CSRF</strong> pour de bon. Avec <code>SameSite=None</code>, le navigateur ne fait plus ce travail à notre place.</li>
</ul>
<h2>Entra ID ne joue pas les brokers</h2>
<p>Toute cette cinématique, il faut la faire fonctionner pour chaque fournisseur d’identité. D’où l’envie, très vite, de n’en avoir qu’un seul face à l’appli.</p>
<p>Le candidat naturel était Entra ID : c’est l’annuaire du groupe, et il est opéré par une vraie équipe IAM. Mais Entra ID ne sait pas se placer devant un autre fournisseur comme simple relais. Il sait fédérer un fournisseur externe, à une condition : que chaque utilisateur existe aussi chez nous, sous forme de <strong>compte invité</strong> (B2B).</p>
<p>C’est la solution la plus propre côté code — l’appli ne parle plus qu’à un émetteur. Le prix est ailleurs : le cycle de vie de ces comptes invités devient notre affaire. Avec une population de centre de contact et le turnover qui va avec, nous l’avons écartée.</p>
<p>Restaient deux options.</p>
<h2>Option 1 : notre propre Keycloak</h2>
<p>Un Keycloak chez nous, en <em>identity broker</em> : l’appli ne connaît que lui, et c’est lui qui va chercher l’utilisateur chez Entra ID ou chez le prestataire.</p>
<p>Les avantages sont réels :</p>
<ul>
<li><strong>Un seul flux à faire marcher dans l’iframe.</strong> La popup, le message, le cookie : tout est réglé une fois, pour un seul émetteur.</li>
<li><strong>Une appli plus simple.</strong> Une configuration, un contrat de claims, et la fédération d’identités rangée dans le composant dont c’est le métier.</li>
<li><strong>Réutilisable.</strong> La prochaine appli qui devra accueillir ce prestataire n’aura rien à refaire.</li>
</ul>
<p>Le défaut tient en un mot : <strong>gouvernance</strong>. Un broker est un composant critique — haute disponibilité, base de données, montées de version, correctifs de sécurité — et un point de panne commun aux deux populations : s’il tombe, plus personne ne se connecte. Il lui faut un propriétaire.</p>
<p>Or nous avions justement un Keycloak dans le service. Il tournait, mais il n’était réellement opéré par personne. Chez nous, seul Entra ID est porté par l’équipe IAM. Bâtir l’accès de tout un service client sur un composant orphelin, ou monter une exploitation digne de ce nom dans le budget et les délais d’un projet : ni l’un ni l’autre n’était raisonnable.</p>
<h2>Option 2 : l’appli gère les deux</h2>
<p>L’application back-office parle directement aux deux fournisseurs. Pas de nouveau composant, pas de nouvelle dépendance à exploiter, et des pannes cloisonnées : un incident chez le prestataire ne bloque pas nos collaborateurs, et inversement.</p>
<p>Les défauts sont le miroir exact de l’option 1 :</p>
<ul>
<li><strong>Deux flux à faire marcher dans l’iframe</strong>, avec deux fournisseurs qui n’ont pas les mêmes réglages ni les mêmes interlocuteurs.</li>
<li><strong>De la logique d’identité dans l’appli</strong> : deux configurations, deux formats de claims à normaliser.</li>
<li><strong>Rien de réutilisable.</strong> La prochaine appli refera le même travail.</li>
</ul>
<h2>Ce que le multi-IdP demande à l’appli</h2>
<p>Pour que ce choix reste vivable, l’essentiel est d’empêcher les deux fournisseurs de se répandre dans le code.</p>
<ul>
<li><strong>Un seul type d’utilisateur en sortie.</strong> Chaque fournisseur a son convertisseur, et les deux produisent le même objet métier : interne ou externe, rôles de l’appli. Passé la connexion, rien ne doit savoir d’où vient l’utilisateur.</li>
<li><strong>Un identifiant composé</strong> : le couple émetteur et identifiant. Côté Entra ID, l’identifiant d’objet et le tenant plutôt que le <code>sub</code>, qui change d’une application à l’autre.</li>
<li><strong>Des rôles d’application plutôt que des groupes</strong> côté Entra ID : un claim lisible, sans identifiants opaques ni limite de taille. Côté prestataire, des rôles déclarés sur le client.</li>
<li><strong>L’autorisation reste une règle métier.</strong> Un agent du prestataire n’a pas les mêmes droits qu’un collaborateur, et ce n’est pas au fournisseur d’identité d’en décider.</li>
<li><strong>Les jetons s’arrêtent au backend.</strong> Les services en aval sont appelés avec l’identité de l’appli et l’utilisateur normalisé, sinon chacun hérite du multi-IdP.</li>
</ul>
<h2>Ce que j’en retiens</h2>
<p>Nous avons choisi le multi-IdP. Pas parce que c’est la meilleure architecture dans l’absolu : parce qu’il n’existait pas, côté groupe, de solution de fédération solide sur laquelle s’appuyer. À l’heure où j’écris, les tests d’intégration sont en cours, et en bonne voie.</p>
<p>Les deux options se valent sur le papier. Ce qui les départage n’est pas technique : c’est <strong>qui opère quoi</strong>.</p>
<ul>
<li><strong>Une équipe pour porter un broker</strong> → le broker. C’est là que la fédération doit vivre, et chaque nouvelle appli en profite.</li>
<li><strong>Personne pour le porter</strong> → le multi-IdP dans l’appli, proprement isolé. Mieux vaut un peu de complexité dans un code qu’on maîtrise qu’un composant critique sans propriétaire.</li>
<li><strong>Une population externe stable</strong> → les comptes invités, qui simplifient tout le reste.</li>
</ul>
<p>Et le jour où une solution groupe existera, la migration sera courte : si l’appli ne connaît qu’un seul type d’utilisateur, remplacer deux fournisseurs par un seul ne touche que la porte d’entrée.</p>
]]></content:encoded>
  </item>
</channel>
</rss>
