Les quatre minutes d’approbation
Dans le billet précédent, nous avons écrit : « la cryptographie a fait le travail que SSO + 2FA faisait avant. » Plusieurs ingénieurs ont écrit pour demander ce que cela voulait dire concrètement dans le code. Ce billet est la réponse.
Le point de départ était une friction mesurable dans les workflows clients MSP. L’IA propose une saisie de temps ConnectWise facturable. Une manager doit signer avant qu’elle soit postée. Le chemin historique :
- Notification Slack : « approbation requise »
- Cliquer sur le lien → redirection SSO → mot de passe → 2FA → page d’approbation
- Lire l’entrée → cliquer Approuver
Temps médian jusqu’à la première action : 4 minutes. Multipliez par 30 entrées/jour × 4 techniciens dans un MSP typique de 10 places, et vous avez 8 heures par jour d’attention manageriale pour faire ce qui est fonctionnellement un tas de pouces levés.
Les quatre minutes n’étaient pas parce que quelqu’un était lent. Elles étaient parce que le chemin vers l’approbation exigeait une cérémonie d’identité complète pour ce qui était, en termes d’information, un seul bit (approuver/rejeter) plus un texte de raison optionnel.
Nous l’avons remplacé par ceci : l’e-mail de la manager contient des boutons Approuver et Rejeter. Un clic. L’action s’exécute, le journal d’audit enregistre, la saisie de temps est postée vers ConnectWise. Pas de connexion. Pas de 2FA.
Ça passe le questionnaire SOC 2. Voici comment.
Les quatre propriétés qui en font un équivalent auditable
Le lien dans chaque bouton n’est pas une URL avec ?approval_id=42. C’est un JWT (JSON Web Token) signé avec la clé privée de notre serveur, avec quatre propriétés :
- Signé. L’authenticité du token est cryptographiquement vérifiable. Nous ne faisons pas confiance à la barre d’URL ; nous faisons confiance à la signature.
- Borné dans le temps. Chaque token expire (claim
exp), typiquement 72 heures après émission. Après ça, le clic ne fait rien. - Usage unique. Un token peut être réclamé exactement une fois. Le second clic sur le même bouton Approuver tape un cache d’idempotence et renvoie « déjà approuvé par vous à [horodatage] ».
- Idempotent. Quoi que fasse le réseau — clics multiples, retries, prefetch du navigateur — l’action sous-jacente s’exécute au plus une fois. Même effet à chaque fois.
La première propriété est ce qui nous laisse passer SSO. Les trois autres sont ce qui nous laisse passer les questions « mais et si… » de l’équipe d’audit.
Le format wire
Le payload du token (après jwt.decode mais avant la vérification de signature) ressemble à :
{
"iss": "jiegou.ai",
"aud": "approval.action",
"iat": 1714502400,
"exp": 1714761600,
"jti": "01HZ8B3X9NQK5W2T7P0V4M6Y8R",
"approval_id": "appr_2bF7kQpL9xR3",
"approver_email": "alex@example-msp.com",
"action": "approve",
"account_id": "acc_4nM8tYz2",
"scope": "time_entry"
}
Trois choses à propos de ce payload :
- Le token lie l’action à un approbateur spécifique. Même si l’e-mail est transféré, le destinataire ne peut pas le réclamer en tant que lui-même — le journal d’audit enregistre toujours
alex@example-msp.comcomme approbateur, pas le transmetteur. (Plus sur la menace e-mail-transféré ci-dessous.) - Le
jtiest un ULID — unique par token, utilisé comme clé d’idempotence dans notre cache de réclamation. - Le champ
scopeindique au serveur vers quel handler de réclamation dispatcher.time_entrypasse par un chemin de code différent decontent_publishououtbound_message. Même pattern JWT, handlers de scope différents.
L’ordre de validation compte
Quand le clic frappe le serveur, nous validons dans cet ordre exact. Si une étape échoue, on s’arrête :
- Vérifier la signature. C’est la première chose — avant de parser, avant de faire confiance à n’importe quel champ. Si la signature est invalide, la requête reçoit un
400 Bad Requestgénérique sans détail. Nous ne fuitons pas quel champ a échoué. - Vérifier
issetaud. Mauvais émetteur ou audience → rejeter. Ça attrape les tokens forgés par d’autres services ou rejoués entre systèmes. - Vérifier
exp. Expiré → rendre une page « ce lien d’approbation a expiré, merci de demander un nouveau ». - Vérifier
jticontre le cache de réclamation. Déjà réclamé → rendre une réponse d’idempotence « déjà approuvé par [approbateur] à [heure] ». - Chercher l’enregistrement d’approbation. Si l’
approval_idn’existe pas ou a déjà été résolu via un autre chemin (par exemple, approuvé dans la console), rendre « cette approbation a déjà été résolue ». - Faire correspondre
approver_emailà la liste des approbateurs autorisés de l’approbation. Sialex@...n’est pas sur la liste des approbateurs de la politique de cette approbation, rejeter avec403. - Exécuter l’action. Atomiquement : écrire l’enregistrement de réclamation (
jti→ réclamé-à), émettre l’entrée d’auditapproval_event, dispatcher le workflow post-approbation (ex. : poster la saisie de temps vers ConnectWise). - Rendre le succès. Afficher une page de confirmation ; ne pas réémettre de tokens ; ne rien stocker dans la barre d’URL qui pourrait être re-partagé.
Le choix non-évident clé est l’étape 1. Vérifier avant de parser. Certaines implémentations font jwt.decode d’abord pour extraire le kid (key ID) pour la vérification de signature, mais ça veut dire que vous avez déjà fait confiance à des données non signées. Utilisez une librairie JWT qui vérifie en un seul appel et rejette les tokens malformés avant d’exposer les claims.
Le modèle de menace — quatre choses, ce que chacune coûte à l’attaquant
Nous nous sommes assis avec les quatre attaques les plus plausibles et avons demandé : comment notre design gère-t-il chacune ?
1. L’e-mail est transféré à quelqu’un en dehors de l’entreprise
Ce qui se passe : quelqu’un d’autre que alex@... clique sur le bouton.
Coût pour l’attaquant : zéro — il a le token en clair.
Mitigation : le token lie l’action à l’identité alex@... dans le journal d’audit. Le transmetteur ne peut pas le réclamer en tant que lui-même. L’action s’exécute, mais elle s’exécute en tant qu’Alex. La prochaine revue de gouvernance fait apparaître « Alex a approuvé ceci depuis l’IP 203.0.113.7 (qui n’est pas le bureau d’Alex) » — et Alex le découvre immédiatement à sa prochaine connexion.
C’est ce que nous n’empêchons pas pleinement par cryptographie. Nous l’empêchons opérationnellement : détection d’anomalies sur l’IP/UA de l’approbateur, résumé de gouvernance hebdomadaire, et une page de confirmation de 3 secondes qui dit « vous êtes sur le point d’approuver en tant qu’alex@example-msp.com — continuer ? ». La fraude par transfert est détectable, pas évitable, avec les liens e-mail. Nous le documentons clairement dans le questionnaire SOC 2 client.
2. Le token fuit dans un journal serveur quelque part
Ce qui se passe : un token apparaît dans notre log d’accès nginx, ou un log de webhook tiers, ou un console.log égaré.
Coût pour l’attaquant : dépend de la fraîcheur.
Mitigation : trois couches.
- Court
exp. L’expiration de 72 heures signifie qu’un token fuité est mort dans les 3 jours. Nous n’émettons pas de tokens de 30 jours. - Réclamation à usage unique. Si l’utilisateur légitime a cliqué d’abord, le token est mort. Le lookup du cache de réclamation se passe avant tout effet de bord.
- Hygiène de logging. Les URLs de liens d’action sont pattern-matchées et caviardées dans nos logs d’accès et l’ingestion SIEM. Les tokens n’arrivent jamais dans le stockage durable en dehors du JWT lui-même.
3. Replay — même token cliqué deux fois
Ce qui se passe : retries réseau, double-clics, prefetch navigateur, ou un replay malicieux.
Coût pour l’attaquant : zéro (les replays légitimes sont communs).
Mitigation : la vérification du cache de réclamation à l’étape 4 de la validation est porteuse. Le cache est Redis, indexé sur jti, avec un TTL plus long que exp. La première réclamation pose la clé atomiquement (SETNX) ; chaque clic suivant lit l’enregistrement existant et renvoie la réponse idempotente « déjà approuvé », incluant qui a approuvé et quand.
L’idempotence n’est pas juste une commodité UX — c’est la propriété qui laisse le système être sûr sous retry sans fuiter d’état.
4. Notre clé de signature est compromise
Ce qui se passe : le secret HMAC ou la clé privée RSA fuit.
Coût pour l’attaquant : il peut forger des tokens arbitraires pour n’importe quelle approbation.
Mitigation : quatre couches.
- Isolation de clé. La clé de signature vit dans notre KMS, pas dans la mémoire applicative. Les serveurs applicatifs appellent le KMS pour signer et vérifier ; ils ne possèdent jamais le matériel de clé brut.
- Rotation de clé. Les clés tournent selon un calendrier publié ; les anciennes clés sont acceptées en vérification seulement pendant une fenêtre de grâce égale au plus long
expémis. - Tokens scopés par approbateur. Même avec compromission de clé, un attaquant qui forge un token pour
alex@...et clique dessus produit une entrée d’audit sous l’identité d’Alex, ce qui (par la menace 1) est détectable. - Réponse à compromission. Si nous détectons une compromission de clé, nous révoquons la clé et invalidons tous les tokens en suspens. Les clients voient « ce lien d’approbation a été invalidé pour des raisons de sécurité ; merci d’utiliser la console pour résoudre » jusqu’à réémission.
C’est la menace la plus difficile à pleinement mitiger, et c’est pourquoi le système a un fallback : la queue d’approbation in-console fonctionne toujours. Les approbations par bouton e-mail sont un ajout, pas un remplacement.
Le compromis honnête face à SSO + 2FA
Les approbations par bouton e-mail ne sont pas strictement équivalentes à SSO + 2FA. Le compromis :
| Propriété | SSO + 2FA | Lien d’action signé |
|---|---|---|
| Authentifie l’identité | Oui (facteur + facteur) | Non — authentifie une décision unique liée à une identité |
| Résiste au transfert d’e-mail | Oui (l’utilisateur transféré ne peut pas passer 2FA) | Détectable mais pas évitable |
| Résiste à la compromission de clé | Indépendant de notre infra (IdP fédéré) | Dépend de notre hygiène KMS |
| Friction par approbation | 4 minutes | moins de 5 secondes |
| Équivalent en piste d’audit | Oui | Oui (même forme approval_event) |
Nous sommes explicites avec les clients sur quel compromis ils choisissent. La configuration recommandée :
- Approbations à faible enjeu, gros volume (saisies de temps, brouillons de contenu, holds de calendrier) : bouton e-mail par défaut
- Approbations à fort enjeu (transactions financières au-dessus d’un seuil, engagements visibles à l’extérieur, actions affectant la conformité) : exiger console + SSO ; le bouton e-mail revient à un « rappel » seulement
- Surcharge par compte : chaque client peut définir le seuil pour quelles actions reçoivent un bouton e-mail vs. console-seule
Le seuil est une valeur de config. La cryptographie est la même.
Équivalence du journal d’audit
Chaque réclamation de lien d’action signé émet la même forme approval_event qu’une approbation console. Le champ entry-point lit email_button ; le reste est identique :
{
"event_type": "approval.resolved",
"approval_id": "appr_2bF7kQpL9xR3",
"approver_email": "alex@example-msp.com",
"decision": "approve",
"decision_at": "2026-04-29T14:32:18Z",
"entry_point": "email_button",
"client_ip": "203.0.113.42",
"user_agent": "Mozilla/5.0 …",
"jti": "01HZ8B3X9NQK5W2T7P0V4M6Y8R"
}
Le entry_point est la seule chose que l’auditeur a besoin de savoir que ça vient d’un e-mail. Tout le reste — qui, quand, quoi, IP — correspond à la preuve d’approbation console. Notre format d’export d’évidence SOC 2 les traite identiquement. L’auditeur lit un schéma.
Cinq autres workflows qui correspondent à ce pattern
Une fois le pattern JWT-cache-de-réclamation existant, nous avons trouvé qu’il s’adaptait à beaucoup de workflows à décision unique :
- Confirmations de hold de calendrier — l’IA propose « bloquer 30 minutes mercredi à 14h pour la préparation de la revue Q3 » ; l’opérateur confirme ou déplace
- Hold-and-release de publication de contenu — brouillons qui passent les checks auto mais sont marqués pour confirmation de l’expéditeur
- Holds de message sortant — l’IA brouillonne une réponse client ; l’opérateur la libère depuis une queue retenue
- Déverrouillages d’intégration sensible — l’IA demande la permission d’utiliser un outil spécifique qui est derrière une porte d’approbation par utilisation
- Flux de consentement uniques — consentement face client pour des actions qui affectent leurs données
Chacun nécessitait auparavant une visite console. Chacun a maintenant un équivalent bouton e-mail. La cryptographie est la credential. La journée de l’opérateur rétrécit en conséquence.
Ce que nous dirions à une autre équipe en train de construire ça
Si vous construisez quelque chose de similaire, trois choses à embrasser et trois à éviter :
Embrasser :
- Vérification de signature JWT avant tout parsing de champ. Utilisez une librairie qui fait ça en un seul appel.
- Idempotence du cache de réclamation indexée sur
jti. C’est ce qui rend retry-safe. - Même forme d’événement d’audit indépendamment du point d’entrée. Les auditeurs ne devraient pas se soucier de quel chemin vient le clic.
Éviter :
- Long
exppour « être gentil ». Un lien d’approbation de 30 jours est une surface d’attaque de 30 jours. 72 heures est largement suffisant pour des approbateurs humains. - Stocker le token dans un fragment d’URL en espérant que les navigateurs ne le logueront pas. Ils le feront. Traitez les URLs comme des logs-éventuellement.
- Modes d’échec silencieux. Quand la vérification échoue, rendez une page d’erreur claire. L’approbateur légitime peut être sur un vieux token parce qu’il a ignoré l’e-mail pendant une semaine.
Le slogan du billet précédent était : la confiance n’a pas faibli, elle s’est relocalisée vers une couche avec moins de friction. Les liens d’action signés en sont l’exemple littéral. La confiance s’est déplacée de SSO + 2FA + gestionnaire de mots de passe + session navigateur vers clé de signature bien gérée + JWT à exp court + cache de réclamation idempotent. La friction est passée de 4 minutes à moins de 5 secondes. La piste d’audit a gagné plus de champs, pas moins.
Commencez gratuitement → — ou si vous opérez un MSP et que le problème d’approbation de 4 minutes ci-dessus vous semble familier, réservez un walkthrough de 30 minutes et nous configurerons votre compte avec les approbations par bouton e-mail dès le premier jour.