Magic link : comment marche une connexion sans mot de passe
Ou : Comment transformer un problème de sécurité en problème de boîte mail, et appeler ça un progrès
Il existe une façon de regarder les mots de passe qui rend tout le reste plus clair. Un mot de passe, ce n’est pas un mécanisme de sécurité. C’est une dette que tu émets et que l’utilisateur rembourse à ta place. Tu lui demandes de mémoriser une chaîne qu’il n’a pas choisie, de ne jamais la réutiliser, de la changer quand tu paniques, et de ne pas la coller dans un fichier notes.txt. En échange, tu gardes une empreinte dans ta base et tu dors mieux.
Il rembourse mal, évidemment. Il réutilise, il note, il oublie. Alors tu ajoutes un formulaire de réinitialisation, qui envoie un lien par mail, qui permet de se connecter. Et le jour où tu regardes ce formulaire en face, tu réalises quelque chose d’inconfortable : cette porte de secours est déjà un système d’authentification complet. Elle fonctionne. Elle est utilisée. Elle contourne le mot de passe que tu as passé trois semaines à sécuriser.
Le magic link, c’est la décision d’arrêter l’hypocrisie et d’en faire la porte principale.
C’est la décision que j’ai prise en construisant l’authentification de Presentation Control, et le retour d’expérience de ce système de login raconte les deux weekends qu’elle a coûtés. Cette série reprend le sujet depuis le début, proprement, en Go.
Magic link c’est quoi : la connexion sans mot de passe, expliquée de bout en bout
Un magic link est un lien à usage unique, envoyé par courrier électronique, qui connecte son destinataire quand il clique dessus. Pas de champ mot de passe, pas de « confirmez votre mot de passe », pas de politique de complexité que personne ne lit. L’utilisateur saisit son adresse, reçoit un message, clique, il est connecté. Cette mécanique porte plusieurs noms selon les vendeurs, connexion sans mot de passe, authentification passwordless, lien magique, mais la promesse est identique : déplacer la preuve d’identité du cerveau de l’utilisateur vers un canal qu’il possède déjà.
Le reste de cet article démonte ce déplacement. Ce qu’il coûte, ce qu’il rapporte, et à quoi ressemble une implémentation qui ne s’écroule pas au premier attaquant motivé.
Les six étapes, et celle que personne ne regarde
Le flux tient en six temps.
L’utilisateur saisit son adresse. Le serveur fabrique un secret aléatoire, en garde une empreinte et l’associe à un compte avec une date d’expiration. Il envoie un message contenant le secret dans l’URL. L’utilisateur clique. Le serveur retrouve l’empreinte, vérifie qu’elle n’a pas expiré, la détruit, et ouvre une session. Six étapes, dont cinq relèvent de ton code.
La sixième n’est pas dans cette liste, parce qu’elle ne t’appartient pas. Entre l’envoi et le clic, ton secret traverse un serveur SMTP, dort dans une boîte mail, transite peut-être par un antivirus d’entreprise qui visite les liens pour vérifier qu’ils sont sains1. Tu as transféré ta dette de sécurité au fournisseur de messagerie de ton utilisateur. C’est un transfert honnête, à condition de savoir que tu l’as fait.
Parce qu’il en découle une phrase qu’il faut accepter avant d’écrire la première ligne : qui contrôle la boîte mail contrôle le compte. Ce n’est pas un défaut de conception, c’est la conception. Un attaquant qui lit les messages de ta cible n’a pas besoin de casser ton système, il attend le lien. Et si ça te choque, regarde ton formulaire de réinitialisation de mot de passe : il offre exactement la même prise, avec une étape de plus et l’illusion d’un rempart.
Ce qu’un bon lien garantit
Un magic link correct respecte cinq propriétés, et chacune correspond à une attaque précise.
| Propriété | Ce qu’elle empêche |
|---|---|
| Durée de vie courte | Un lien retrouvé six mois plus tard dans une archive |
| Usage unique | Le rejeu du même lien par quelqu’un d’autre |
| Impossible à deviner | La fabrication d’un lien valide par force brute |
| Stocké haché | La lecture d’une base compromise pour se connecter |
| Réponse constante | La découverte de qui possède un compte chez toi |
Les trois premières sont évidentes et personne ne les rate. Les deux dernières sont celles qui séparent un prototype d’un système.
Stocker le secret en clair dans ta base revient à stocker des mots de passe en clair, avec la circonstance aggravante que tu croyais avoir supprimé le problème. On conserve une empreinte, on compare des empreintes.
Quant à la réponse constante, elle règle un problème dont on parle peu. Si ton endpoint répond « lien envoyé » pour une adresse connue et « compte introuvable » pour une autre, tu viens d’offrir un service de vérification d’adresses gratuit. Injecte une liste de dix mille adresses, note lesquelles existent, revends le résultat. Ta page de connexion est devenue un produit. Les recommandations d’authentification de l’OWASP en font un point explicite2, et c’est le sujet entier du troisième article de cette série.
L’architecture, en trois interfaces
Voici où la plupart des tutoriels partent en vrille. Ils ouvrent une connexion Postgres à la ligne 4, appellent un client SMTP à la ligne 40, et produisent quelque chose d’impossible à tester sans base de données ni serveur de mail.
Le système que je construis dans cette série tient sur trois contrats et rien d’autre.
| |
Trois interfaces, neuf lignes utiles. Le service qui les consomme ne sait pas que Postgres existe.
| |
Ce n’est pas de l’élégance gratuite. Consume porte à lui seul la propriété d’usage unique : c’est une opération qui lit et supprime dans le même mouvement, et son implémentation Postgres sera un DELETE ... RETURNING plutôt qu’un SELECT suivi d’un DELETE, parce que deux clics simultanés sur le même lien ne doivent pas ouvrir deux sessions. Clock existe pour une raison encore plus terre à terre : tester qu’un lien expire au bout de quinze minutes sans attendre quinze minutes. Ce découpage par interfaces, c’est la frontière testable dont je parlais à propos du mocking en Go, appliquée à un cas où elle rapporte immédiatement.
Pourquoi la bibliothèque standard suffit
Le réflexe moderne consiste à chercher une bibliothèque d’authentification. Pour un magic link, le compte est vite fait.
Générer le secret demande crypto/rand, qui puise dans le générateur du système d’exploitation. Trente-deux octets aléatoires encodés en base64 URL-safe donnent un identifiant qu’aucune force brute ne devine. Le hacher demande crypto/sha256, suffisant ici parce qu’on protège un secret à haute entropie et à durée de vie courte, contrairement à un mot de passe humain qui exige un algorithme lent3. Le comparer demande crypto/subtle, dont la fonction ConstantTimeCompare évite qu’un attaquant déduise le secret en chronométrant tes réponses. Servir le tout demande net/http. Le seul ajout extérieur de la série sera pgx pour parler à Postgres.
Prendre un framework d’authentification reste une décision défendable. Elle devient discutable quand elle est prise par réflexe, pour un besoin que quatre paquets de la bibliothèque standard couvrent déjà, au prix d’une dépendance qui aura ses propres avis sur ton schéma de base et ses propres failles à surveiller.
Le moment où ça devient concret
DevOps Dave : J’ai mis le magic link en prod. Plus de mots de passe, plus de tickets « j’ai oublié le mien ». Les utilisateurs adorent.
Security Sarah : Tu stockes quoi dans la table de tokens ?
DevOps Dave : Le token. Comme ça je compare directement quand il revient.
Security Sarah : Donc si quelqu’un lit ta base, il n’a pas besoin de la craquer. Il a juste à cliquer.
DevOps Dave : …
Security Sarah : Et quand quelqu’un demande un lien pour une adresse qui n’existe pas, tu réponds quoi ?
DevOps Dave : « Aucun compte associé à cette adresse. » C’est plus clair pour l’utilisateur, non ?
Security Sarah : C’est surtout plus clair pour celui qui teste ta base clients avec une liste d’adresses achetée.
Dave n’a rien fait d’absurde. Les deux erreurs qu’il vient de commettre sont celles qu’on trouve dans la majorité des implémentations maison, et aucune des deux ne produit de bug visible. Le système marche. Il marche même très bien, jusqu’au jour où il marche pour quelqu’un d’autre.
Et les alternatives, alors
La question arrive toujours, et elle mérite mieux qu’un tableau de fin d’article. Voici quand même le tableau, en attendant le traitement sérieux.
| Méthode | Ce qu’elle suppose | Son vrai coût |
|---|---|---|
| Magic link | Accès à la boîte mail | La latence du courrier, et la confiance déléguée au fournisseur de messagerie |
| Passkeys | Un appareil compatible et synchronisé | La récupération quand l’appareil est perdu |
| OTP par SMS | Une carte SIM sous contrôle | L’échange de carte SIM, et le coût par message |
| OAuth | Un compte chez un tiers | La dépendance à ce tiers, ses pannes et ses décisions |
Aucune ligne de ce tableau ne supprime le problème de l’authentification. Chacune choisit à qui déléguer la confiance, et cette délégation est le sujet entier du cinquième article. La question n’est jamais « quelle méthode est la plus sûre », elle est « à qui suis-je prêt à confier la porte, et qu’est-ce que je fais le jour où cette entité me laisse tomber ».
La suite
Quatre articles suivent celui-ci, et ils construisent un seul système, dans l’ordre du cycle de vie d’une requête. Le deuxième fabrique le token : crypto/rand, hachage, durée de vie, et la raison pour laquelle on ne stocke jamais ce qu’on envoie. Le troisième traite la demande : anti-énumération, limitation de débit, envoi du message. Le quatrième vérifie le clic : consommation atomique, cookie de session, et le choix entre session serveur et jeton signé. Le cinquième compare enfin les quatre méthodes ci-dessus, une fois que le coût réel d’une implémentation propre est sous les yeux.
Le tout en bibliothèque standard, avec pgx pour la base, sans framework et sans dépôt public. Les trois interfaces de cet article ne bougeront plus.
Le magic link ne supprime donc pas le problème du mot de passe. Il le déménage vers la boîte mail, où il était déjà à moitié installé par ton formulaire de réinitialisation. Ce qui change, c’est qu’à partir de maintenant, tu le sais.
Les passerelles de sécurité des messageries d’entreprise suivent les liens contenus dans les messages pour en évaluer la dangerosité. Sur un lien à usage unique, cette visite automatique le consomme avant l’utilisateur, qui reçoit un message d’erreur pour un lien qu’il n’a jamais ouvert. Le contournement habituel consiste à exiger une action explicite sur une page intermédiaire plutôt que de connecter directement sur la requête GET. C’est le genre de détail qu’on découvre en production, généralement un vendredi. ↩︎
L’OWASP Authentication Cheat Sheet consacre une section aux messages d’erreur génériques, avec le même raisonnement : une réponse qui varie selon l’existence du compte est une fuite d’information, quelle que soit la méthode d’authentification. ↩︎
La distinction tient à l’entropie de ce qu’on protège. Un mot de passe humain est devinable, donc on le passe dans une fonction volontairement lente, bcrypt ou argon2, pour rendre chaque essai coûteux. Un token de 32 octets tirés au hasard n’est pas devinable, et le hacher sert uniquement à ce qu’une base volée ne contienne pas de secrets utilisables. SHA-256 fait le travail sans ralentir la vérification. ↩︎