*J'ai construit mon propre gestionnaire de passkeys et maintenant je comprends pourquoi personne ne le fait à la va‑vite

15 min read31 août 2026

Comment j'ai construit passkey-vault : ce qui fait le plus mal dans WebAuthn et les décisions à prendre en tant que fondateur, pas seulement développeur.

Sujets: passkeys · webauthn · security · fido2

Introduction

Salut ! Tout a commencé par de la rage, comme d’habitude. Je me connecte avec un passkey, le navigateur me demande quel gestionnaire de mots de passe doit le prendre en charge, je choisis, j’attends, ça plante, je réessaie via le téléphone, je scanne le QR, rien. Encore une fois. Putain. Après la troisième session de la semaine, je me suis dit : ce n’est pas une technologie qui doit fonctionner « presque toujours ». Ça doit fonctionner tout le temps, parce que c’est de l’authentification. Du coup, au lieu de maudire encore un produit tiers, j’ai ouvert un dossier vide et j’ai commencé à coder le mien.

C’est ainsi qu’est né passkey-vault. Ce post n’est ni un changelog ni un tutoriel pas‑à‑pas. C’est ce que personne ne te dira avant que tu ne te mets à jouer avec WebAuthn : la démo du site du fabricant et l’implémentation réelle sont deux mondes différents, et la plupart des décisions difficiles dans ce projet n’étaient pas techniques, mais de fondateur.

Tu vas obtenir trois choses :

  1. Ce qui fait vraiment mal dans WebAuthn, quand tu arrêtes de copier les exemples de la doc et que tu commences à bâtir quelque chose qui doit survivre aux vrais utilisateurs,
  2. Les décisions que j’ai dû prendre en tant que fondateur, pas développeur : quoi sacrifier au départ, à qui faire confiance, quoi construire soi‑même et quoi acheter,
  3. Quand construire son propre gestionnaire de passkeys c’est de la folie, parce que, honnêtement, dans la plupart des cas, ça ne l’est pas.

L’app iOS qui en est sortie mérite un article à part. Ici, je reste sur le moteur et le processus qui y mène.

Qu’est‑ce que ce passkey‑vault au juste

En bref : un gestionnaire de clés d’accès, c’est‑à‑dire une implémentation relying party et du stockage côté serveur, qui gère l’enregistrement et la connexion avec des passkeys selon WebAuthn et FIDO2, plus une couche de synchronisation pour que le même utilisateur puisse se connecter depuis plusieurs appareils sans devoir scanner un QR à chaque fois. Ça sonne comme une phrase. En pratique, c’est une vingtaine de petites décisions, chacune avec des conséquences sécuritaires si tu te plantes ne serait‑ce qu’une fois.

Avant que quelqu’un ne demande : oui, fonctionnellement ça ressemble à un autre Bitwarden, parce qu’en plus des passkeys il stocke les mots de passe classiques, les codes TOTP, des dossiers pour organiser le bazar, un générateur de mots de passe, des notes et la possibilité de partager un mot de passe avec un contact de confiance sans passer par Messenger. Ce post ne traite que de la couche WebAuthn, car c’est le morceau le plus difficile à bien faire, mais le reste du produit existe et fonctionne, même s’il mérite son propre article. La différence n’est pas dans la liste des fonctionnalités, mais dans le pourquoi de son existence : pas pour concurrencer le marché, mais pour arrêter de payer pour les limites imposées par d’autres et disposer d’un outil qui m’appartient. J’en parlerai plus dans la section des décisions de fondateur.

Problème n° 1 : la démo de la documentation ment en omettant des choses

Chaque tutoriel WebAuthn ressemble à peu près à ça :

const credential = await navigator.credentials.create({
  publicKey: {
    challenge: new Uint8Array(32),
    rp: { name: "passkey-vault", id: "passkey-vault.example" },
    user: { id: userId, name: email, displayName: email },
    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
  },
});

Joli, court, ça marche en local dans la démo. Le problème apparaît quand rp.id doit correspondre exactement au domaine depuis lequel la page est servie, en comptant le registrable domain, pas n’importe quel sous‑domaine. Tu as app.passkey-vault.com et api.passkey-vault.com, tu veux que le credential fonctionne sur les deux ? Tu dois explicitement mettre rp.id sur le suffixe commun, pas copier ce qui a marché en local. Tu changes le domaine de prod après coup ? Tous les credentials enregistrés deviennent invalides, car ils sont liés à l’origin de façon permanente. Ce n’est pas un bug à corriger, c’est une propriété du protocole que la démo ne te dira jamais, parce que la démo ne change jamais de domaine.

Le deuxième point qui ne fait mal qu’en pratique : tester en local nécessite soit https://localhost, soit un authenticator virtuel via les DevTools de Chrome, car le navigateur refuse WebAuthn sur du simple HTTP (sauf exception localhost). Ma première tentative d’intégration consistait à mocker tout navigator.credentials. Rapidement, j’ai vu que le mock ne capturait que la moitié des erreurs réelles, parce que le vrai authenticator a ses propres bizarreries : certains clés matérielles ne supportent pas les discoverable credentials, certaines implémentations platformes cachent l’état entre les essais, de sorte que deux tests consécutifs dans le même run CI voient des choses différentes. Au final, je suis passé à l’authenticator virtuel via le Chrome DevTools Protocol, et c’est seulement là que les tests ont commencé à attraper du réel.

Problème n° 2 : ce que tu stockes réellement dans la base

C’est le moment où les gens se trompent le plus, moi y compris au départ. La clé privée NE quitte JAMAIS l’authenticator de l’utilisateur, donc le serveur ne la voit jamais et ne la stocke pas. Ce qui atterrit réellement dans ta base, c’est : credentialId, la clé publique, le compteur de signatures (signCount), les drapeaux backup eligible et backup state, et éventuellement les métadonnées d’attestation. Ça paraît anodin, jusqu’à ce que tu te demandes ce qui est sensible.

Réponse : credentialId n’est pas un secret, mais c’est un identifiant qui doit être traité comme des données personnelles, car il lie un utilisateur à un appareil précis. Le compteur de signatures est ta seule ligne de défense contre un authenticator matériel cloné : si le compteur lors d’une connexion suivante est inférieur ou égal au précédent, c’est le signal qu’une copie de la clé a été utilisée et la connexion doit échouer. Le problème, c’est que les passkeys synchronisés dans le cloud (iCloud Keychain, Google Password Manager) renvoient souvent signCount à zéro à chaque connexion, parce que le compteur n’a aucun sens quand le même credential vit sur plusieurs appareils simultanément. Donc la logique « le compteur doit augmenter » se casse exactement dans le cas qui était censé être le plus pratique. J’ai dû séparer deux modes de vérification : strict pour les clés matérielles avec compteur réel, souple pour les credentials marqués backup eligible, où un compteur à zéro est normal et non une alerte.

Problème n° 3 : connexion sans nom d’utilisateur, à repenser deux fois

Les discoverable credentials, c’est‑à‑dire les passkeys que le navigateur peut proposer sans que l’utilisateur saisisse son login, c’est l’argument de vente numéro un des passkeys. Et à juste titre, l’UX est meilleur. Mais bien les implémenter nécessite que le serveur puisse générer un challenge de connexion SANS connaître l’identité de l’utilisateur, parce que, par définition, tu ne la connais pas encore. Ma première version de l’endpoint de connexion partait du postulat silencieux : d’abord identifier l’utilisateur, puis générer les options. Ça marchait pour la connexion avec login, mais ça plantait immédiatement pour le bouton « se connecter avec un passkey » sans champ email.

J’ai donc refondu le flux en deux étapes : un endpoint qui génère les options sans aucun contexte utilisateur, puis un endpoint de vérification qui, après réception de la réponse de l’authenticator, retrouve l’utilisateur via le credentialId retourné. Simple à expliquer en une phrase, mais c’est exactement le type de changement où il faut réécrire la validation côté serveur, parce qu’avant on partait du principe que la session portait déjà l’identité. Ici, l’identité arrive à la fin, pas au début.

Où le téléphone entre en jeu : sync entre appareils

Le plus gros problème réel que j’ai rencontré n’était pas cryptographique, mais de distribution : comment faire en sorte qu’un passkey enregistré sur le laptop puisse réellement être utilisé sur le téléphone sans devoir se réenregistrer à chaque fois. La réponse native des plateformes, c’est le hybrid transport : connexion depuis le téléphone vers le navigateur sur un autre appareil via QR et Bluetooth comme canal de proximité (caBLE), ou synchronisation complète via le fournisseur système, iCloud Keychain côté Apple, Google Password Manager côté Android.

C’est à ce moment que l’on parle de l’app iOS, de l’extension credential provider, de l’intégration avec l’Autofill système, tout ce morceau UX sur l’appareil. C’est une histoire à part, donc je la développerai dans le prochain article. Retiens juste ceci : la décision de supporter le hybrid transport plutôt que d’exiger une ré‑enregistrement sur chaque appareil était une décision produit, pas technique, et je ne l’ai prise qu’après m’être énervé contre mon propre MVP qui me forçait à me ré‑inscrire à chaque fois.

Décisions que j’ai prises en tant que fondateur, pas développeur

Voici la partie que la documentation WebAuthn ne couvre jamais, parce qu’elle ne sait pas que tu construis un produit, pas juste un exercice de crypto.

Ce qu’il faut construire soi‑même vs ce qu’il faut acheter. J’ai laissé la cryptographie de la signature et de la vérification à une bibliothèque, parce que ré‑implémenter CBOR et COSE à la main, c’est exactement le type de boulot où une erreur coûte plus cher que le temps gagné. En revanche, la logique de stockage, le modèle de données, la politique du compteur de signatures et tout le flux d’enregistrement et de connexion, je les ai écrits moi-même, car c’est là que réside la vraie valeur différenciante du produit, pas un truc à externaliser.

Quand arrêter d’ajouter l’attestation. WebAuthn supporte l’attestation, c’est‑à‑dire la preuve cryptographique qu’un credential provient d’un modèle de clé matériel certifié. Ça semble indispensable. Le problème : vérifier l’attestation implique de maintenir à jour les métadonnées des fabricants (FIDO Metadata Service), ce qui est un boulot à part entière, et la plupart des utilisateurs réels utilisent le passkey de plateforme, pas une clé matérielle certifiée, donc l’attestation revient souvent à none. J’ai donc laissé de côté la vérification stricte de l’attestation au départ, en la notant comme dette technique consciente, pas comme une omission.

Pourquoi construire ça soi‑même alors que Bitwarden et 1Password l’ont déjà. Honnêtement ? Le principal motif n’était pas de comprendre le protocole à la racine (même si c’est vrai), mais d’en avoir assez de payer chaque mois pour un outil tiers avec des limites que quelqu’un d’autre a imposées. Avant, j’étais sur NordPass, où le plan que je pouvais me permettre ne me permettait que deux appareils connectés simultanément. Deux. Aujourd’hui, j’ai un laptop, un téléphone, un deuxième téléphone de test et parfois une tablette, et c’est un plafond qui fait mal tous les jours, pas une fois par an. Au lieu de renouveler sans cesse l’abonnement et d’accepter les décisions d’un tiers sur le nombre d’appareils autorisés, j’ai décidé de créer quelque chose où je fixe la limite : pratiquement aucune. C’est la différence entre louer un outil et le posséder. Le fait que je comprenne maintenant chaque ligne de code qui sécurise la connexion de mes futurs utilisateurs est un vrai plus, mais c’est un effet secondaire, pas le point de départ. Honnêtement, une partie de cette décision relève du syndrome « not invented here », mais l’autre partie est simplement un calcul : je préfère payer une fois, de temps en temps, que de dépenser chaque mois pour quelque chose qui me restreint.

Quand construire son propre gestionnaire de passkeys N’EST PAS pertinent

Honnêtement, ce post pourrait facilement être lu comme une incitation à tous à se lancer dans la même folie. Ne construis pas ton propre gestionnaire de passkeys si :

  • les passkeys ne sont qu’un des nombreux moyens de connexion, et pas le pilier de ton produit. Dans ce cas, intégrer un fournisseur prêt à l’emploi (Clerk, Auth0, WorkOS, etc.) te coûte une journée, alors que développer ta propre solution prend des mois et t’expose à des failles que le fournisseur a déjà corrigées.
  • tu n’as pas le budget temps pour suivre l’évolution du standard. WebAuthn et FIDO2 évoluent, de nouvelles extensions comme PRF arrivent dans les navigateurs, et il faut quelqu’un pour les suivre.
  • ton équipe ne compte personne qui aime lire les spécifications cryptographiques pour le plaisir. Sérieusement. Ce n’est pas un projet « on le fait une fois et on oublie », c’est un projet qui nécessite une personne qui le bichonne en permanence.

Si aucun de ces trois points ne s’applique à toi, c’est‑à‑dire que les passkeys sont le cœur de ce que tu construis, que tu as le temps de maintenir, et que quelqu’un dans l’équipe adore les specs, alors construire ton propre gestionnaire commence à avoir du sens. Pour moi, les trois conditions étaient réunies. Ça ne veut pas dire que ça marchera chez toi.

Ce que ça a donné

Passkey‑vault fonctionne, gère l’enregistrement, la connexion sans nom d’utilisateur, et la synchronisation de base via le hybrid transport. Il ne supporte pas encore la vérification complète de l’attestation, et c’est un choix conscient, pas un oubli. La plus grande leçon du projet n’est pas cryptographique, mais le nombre de décisions qui semblent techniques et qui sont en réalité des décisions produit déguisées : quoi sacrifier, à qui faire confiance, pour qui tu construis tout ça.

FAQ

En quoi un passkey diffère d’un mot de passe classique côté serveur ?
Le serveur ne voit jamais la clé privée ; elle reste dans l’authenticator de l’utilisateur. Ce qui arrive côté serveur, c’est : l’identifiant du credential, la clé publique, le compteur de signatures et les drapeaux de backup. C’est un modèle de menace fondamentalement différent d’une base de hashes de mots de passe : même une fuite complète de ta base ne donne rien à un attaquant pour se connecter.

Construire son propre backend WebAuthn est‑il sûr si je ne suis pas expert en crypto ?
Laisse la crypto (vérification des signatures, parsing CBOR) à une bibliothèque éprouvée. Ce que tu construis toi‑même, c’est la logique métier autour : stockage, politique du compteur, flux de connexion. Là aussi on peut faire des erreurs, mais elles sont plus faciles à raisonner et à tester que de ré‑implémenter des primitives cryptographiques.

Pourquoi le compteur de signatures est parfois zéro à chaque connexion ?
Parce qu’un passkey synchronisé dans le cloud entre plusieurs appareils n’a pas de compteur unique qui augmente, le même credential étant utilisé simultanément depuis plusieurs lieux. C’est normal pour les credentials marqués backup eligible et backup state. La règle « le compteur doit augmenter » ne s’applique qu’aux clés matérielles sans synchronisation.

Qu’en est‑il de l’app iOS, du credential provider et de l’intégration Autofill ?
C’est un sujet à part, qui mérite son propre article, car l’intégration avec l’Autofill système sur iOS a un tout autre jeu de problèmes que le backend WebAuthn. Je reviendrai là‑dessus dans le prochain post.

Est‑ce que ça a du sens comme produit ou c’est juste un projet d’apprentissage ?
Ça a commencé comme un projet pour comprendre le protocole. En cours de route, ça a commencé à ressembler à un produit autonome, surtout parce que la frustration initiale était réelle et touchait plus de monde que moi. Si ça deviendra réellement un produit, on verra. Pour l’instant, c’est un moteur solide que je comprends de fond.

Conclusion

Voilà, c’est tout. L’ironie de la situation, c’est que j’ai lancé ce projet parce que j’en avais marre des gestionnaires de mots de passe qui se plantent avec les passkeys. Quelques mois plus tard, j’ai mon propre gestionnaire, où je peux aussi me perdre, mais maintenant je sais exactement pourquoi, parce que je l’ai écrit moi‑même. Ce n’est pas moins frustrant. C’est juste beaucoup plus instructif, et c’est ce qu’on appelle une bonne forme d’échec.

L’app iOS, le credential provider et les dix façons dont l’Autofill sur iPhone peut te surprendre, ce sera pour la prochaine fois. À plus, camarade !

Liens

Continuez a explorer

Pas encore de tags identiques, voici donc les articles les plus recents.