{"id":156,"date":"2026-06-14T20:33:57","date_gmt":"2026-06-14T20:33:57","guid":{"rendered":"https:\/\/shattered.io\/fr\/2026\/06\/14\/authentification-totp-nodejs\/"},"modified":"2026-06-14T20:35:30","modified_gmt":"2026-06-14T20:35:30","slug":"authentification-totp-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/fr\/2026\/06\/14\/authentification-totp-nodejs\/","title":{"rendered":"Authentification TOTP en Node.js : 2FA en 12 \u00e9tapes [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">L&#8217;authentification <strong>TOTP<\/strong> (Time-based One-Time Password) est devenue le second facteur de r\u00e9f\u00e9rence pour prot\u00e9ger les comptes en ligne. Contrairement au SMS, vuln\u00e9rable au SIM swapping, un code TOTP est calcul\u00e9 hors ligne sur l&#8217;appareil de l&#8217;utilisateur \u00e0 partir d&#8217;un secret partag\u00e9 et de l&#8217;heure courante. Dans ce tutoriel publi\u00e9 le 14 juin 2026, vous allez construire, en 12 \u00e9tapes et environ 40 minutes, un syst\u00e8me de double authentification (2FA) complet en Node.js : g\u00e9n\u00e9ration du secret, QR code de provisionnement, v\u00e9rification avec tol\u00e9rance d&#8217;horloge, chiffrement du secret au repos, codes de r\u00e9cup\u00e9ration, limitation des tentatives et protection contre le rejeu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le code est compatible avec Google Authenticator, Microsoft Authenticator, Authy, Ente Auth, 2FAS et tout client conforme \u00e0 la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6238\" target=\"_blank\" rel=\"noopener nofollow\">RFC 6238<\/a>. Vous repartirez avec un projet fonctionnel structur\u00e9 autour d&#8217;une API Express, pr\u00eat \u00e0 int\u00e9grer dans une application r\u00e9elle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"quest-ce-que-le-totp-et-pourquoi-ladopter-en-2026\">Qu&#8217;est-ce que le TOTP et pourquoi l&#8217;adopter en 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le <strong>TOTP<\/strong> est une extension de l&#8217;algorithme HOTP d\u00e9crit par la <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc4226\" target=\"_blank\" rel=\"noopener nofollow\">RFC 4226<\/a> (2005). HOTP g\u00e9n\u00e8re un mot de passe \u00e0 usage unique \u00e0 partir d&#8217;un secret et d&#8217;un compteur incr\u00e9mental. La RFC 6238, publi\u00e9e en 2011, remplace ce compteur par une valeur d\u00e9riv\u00e9e du temps : le syst\u00e8me prend l&#8217;heure Unix courante, la divise par un pas de temps (30 secondes par d\u00e9faut) et utilise le r\u00e9sultat comme compteur. Toutes les 30 secondes, un nouveau code \u00e0 6 chiffres appara\u00eet, identique sur le serveur et sur l&#8217;application mobile tant que leurs horloges sont synchronis\u00e9es.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cette approche \u00e9limine la d\u00e9pendance au r\u00e9seau. Aucun SMS n&#8217;est envoy\u00e9, aucun appel d&#8217;API n&#8217;est n\u00e9cessaire au moment de la validation. Le secret n&#8217;est \u00e9chang\u00e9 qu&#8217;une seule fois, lors de l&#8217;enr\u00f4lement, via un QR code. Ensuite, le t\u00e9l\u00e9phone et le serveur calculent le m\u00eame code chacun de leur c\u00f4t\u00e9. En 2026, l&#8217;ANSSI et l&#8217;ENISA recommandent toujours la 2FA bas\u00e9e sur une application plut\u00f4t que par SMS, jug\u00e9e beaucoup plus r\u00e9sistante \u00e0 l&#8217;interception.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le TOTP pr\u00e9sente trois avantages d\u00e9cisifs pour un d\u00e9veloppeur. Premi\u00e8rement, il est gratuit : aucun co\u00fbt par message, contrairement aux passerelles SMS. Deuxi\u00e8mement, il fonctionne hors ligne, m\u00eame dans un avion ou une zone blanche. Troisi\u00e8mement, il s&#8217;appuie sur des primitives cryptographiques standardis\u00e9es (HMAC-SHA1, et optionnellement SHA-256 ou SHA-512) impl\u00e9ment\u00e9es dans la biblioth\u00e8que standard <code>crypto<\/code> de Node.js. Le seul pr\u00e9requis c\u00f4t\u00e9 utilisateur est une application d&#8217;authentification, d\u00e9sormais install\u00e9e par d\u00e9faut sur la plupart des smartphones.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour replacer le TOTP dans le paysage de l&#8217;authentification forte, comparez-le aux autres m\u00e9thodes courantes. Les passkeys (cl\u00e9s d&#8217;acc\u00e8s FIDO2) offrent une s\u00e9curit\u00e9 sup\u00e9rieure contre le hame\u00e7onnage, mais leur d\u00e9ploiement reste partiel et la 2FA TOTP demeure le filet de s\u00e9curit\u00e9 le plus universel.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>M\u00e9thode 2FA<\/th><th>R\u00e9sistance au hame\u00e7onnage<\/th><th>Hors ligne<\/th><th>Co\u00fbt<\/th><th>Adoption<\/th><\/tr><\/thead><tbody><tr><td>SMS<\/td><td>Faible (SIM swap)<\/td><td>Non<\/td><td>Payant par message<\/td><td>Tr\u00e8s large<\/td><\/tr><tr><td>TOTP (application)<\/td><td>Moyenne<\/td><td>Oui<\/td><td>Gratuit<\/td><td>Large<\/td><\/tr><tr><td>Notification push<\/td><td>Moyenne (fatigue MFA)<\/td><td>Non<\/td><td>Variable<\/td><td>Croissante<\/td><\/tr><tr><td>Cl\u00e9 mat\u00e9rielle FIDO2<\/td><td>\u00c9lev\u00e9e<\/td><td>Oui<\/td><td>Co\u00fbt mat\u00e9riel<\/td><td>En hausse<\/td><\/tr><tr><td>Passkey<\/td><td>\u00c9lev\u00e9e<\/td><td>Oui<\/td><td>Gratuit<\/td><td>\u00c9mergente<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequis-et-versions-logicielles\">Pr\u00e9requis et versions logicielles<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de commencer, installez les versions suivantes. Ce tutoriel a \u00e9t\u00e9 test\u00e9 avec Node.js 24 (ligne LTS \u00ab Krypton \u00bb) et la derni\u00e8re version stable de chaque d\u00e9pendance disponible en juin 2026. V\u00e9rifiez votre version de Node avec <code>node --version<\/code>. Si vous utilisez une version ant\u00e9rieure \u00e0 Node 20, mettez-la \u00e0 jour : l&#8217;API <code>node:crypto<\/code> et la prise en charge native d&#8217;ESM y sont plus stables.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Outil \/ paquet<\/th><th>Version utilis\u00e9e<\/th><th>R\u00f4le dans le projet<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>24.x LTS (Krypton)<\/td><td>Environnement d&#8217;ex\u00e9cution<\/td><\/tr><tr><td>otplib<\/td><td>13.4.1<\/td><td>G\u00e9n\u00e9ration et v\u00e9rification TOTP<\/td><\/tr><tr><td>qrcode<\/td><td>1.5.4<\/td><td>Cr\u00e9ation du QR code de provisionnement<\/td><\/tr><tr><td>express<\/td><td>5.2.1<\/td><td>Serveur d&#8217;API HTTP<\/td><\/tr><tr><td>express-rate-limit<\/td><td>8.5.2<\/td><td>Limitation des tentatives de v\u00e9rification<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Un mot sur le choix des biblioth\u00e8ques. La biblioth\u00e8que <strong>otplib<\/strong> (version 13.4.1, publi\u00e9e fin mai 2026) est activement maintenue et constitue aujourd&#8217;hui la r\u00e9f\u00e9rence pour le TOTP en Node.js. Vous croiserez souvent <code>speakeasy<\/code> dans d&#8217;anciens tutoriels, mais son dernier paquet publi\u00e9 remonte \u00e0 2016 (version 2.0.0). Nous l&#8217;\u00e9vitons ici au profit d&#8217;otplib, plus \u00e0 jour et mieux typ\u00e9. C\u00f4t\u00e9 QR code, <code>qrcode<\/code> 1.5.4 g\u00e9n\u00e8re un data URI directement int\u00e9grable dans une balise image c\u00f4t\u00e9 client.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Vous aurez aussi besoin de notions de base en JavaScript asynchrone (<code>async\/await<\/code>), d&#8217;un terminal et d&#8217;un \u00e9diteur de code. Aucune base de donn\u00e9es n&#8217;est requise pour suivre le tutoriel : nous simulons le stockage avec un objet en m\u00e9moire, puis nous expliquons comment passer en production. Si l&#8217;authentification par jetons vous est inconnue, lisez d&#8217;abord notre guide sur l&#8217;<a href=\"\/fr\/authentification-jwt-nodejs\/\">authentification JWT en Node.js<\/a>, car nous prot\u00e9gerons la route d&#8217;activation 2FA avec une session authentifi\u00e9e.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"comment-fonctionne-lalgorithme-totp-en-detail\">Comment fonctionne l&#8217;algorithme TOTP en d\u00e9tail<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Comprendre l&#8217;algorithme avant de l&#8217;impl\u00e9menter \u00e9vite la majorit\u00e9 des bugs. Le calcul d&#8217;un code TOTP se d\u00e9roule en quatre temps. D&#8217;abord, le serveur et le client partagent un secret al\u00e9atoire d&#8217;au moins 128 bits (160 bits recommand\u00e9s par la RFC 4226), encod\u00e9 en Base32 selon la RFC 4648 pour \u00eatre lisible par les applications d&#8217;authentification. Ensuite, on calcule le compteur temporel : T \u00e9gale l&#8217;heure Unix actuelle moins T0, divis\u00e9 par le pas de temps, T0 valant 0 et le pas valant 30 secondes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Troisi\u00e8me \u00e9tape, on applique HMAC-SHA1 au compteur en utilisant le secret comme cl\u00e9. Le r\u00e9sultat est un condens\u00e9 de 20 octets. Quatri\u00e8me \u00e9tape, la \u00ab troncature dynamique \u00bb extrait 4 octets du condens\u00e9 \u00e0 une position d\u00e9termin\u00e9e par son dernier quartet, convertit ces octets en entier, puis prend le modulo 10 puissance 6 pour obtenir un code \u00e0 6 chiffres. Cet encha\u00eenement est enti\u00e8rement d\u00e9terministe : deux horloges synchronis\u00e9es produisent le m\u00eame code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le param\u00e8tre crucial pour la fiabilit\u00e9 est la <strong>fen\u00eatre de tol\u00e9rance<\/strong>. Les horloges d\u00e9rivent. Si le t\u00e9l\u00e9phone de l&#8217;utilisateur avance de 40 secondes, son code correspond au pas de temps suivant. Pour absorber ce d\u00e9calage, le serveur v\u00e9rifie non seulement le pas de temps courant, mais aussi un ou deux pas avant et apr\u00e8s. Une fen\u00eatre de plus ou moins 1 pas (90 secondes de tol\u00e9rance totale) est un bon compromis entre confort et s\u00e9curit\u00e9. Une fen\u00eatre trop large augmente la surface d&#8217;attaque par force brute.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Voici les param\u00e8tres par d\u00e9faut interop\u00e9rables, ceux que comprennent toutes les applications d&#8217;authentification grand public. Conservez ces valeurs sauf raison pr\u00e9cise, car certaines applications (notamment d&#8217;anciennes versions de Google Authenticator) ignorent les param\u00e8tres non standard et retombent silencieusement sur SHA-1, 6 chiffres et 30 secondes.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Param\u00e8tre<\/th><th>Valeur par d\u00e9faut<\/th><th>Recommandation<\/th><\/tr><\/thead><tbody><tr><td>Algorithme de hachage<\/td><td>SHA-1<\/td><td>SHA-1 pour la compatibilit\u00e9 maximale<\/td><\/tr><tr><td>Nombre de chiffres<\/td><td>6<\/td><td>6 (8 possible mais moins compatible)<\/td><\/tr><tr><td>Pas de temps (period)<\/td><td>30 secondes<\/td><td>30 secondes<\/td><\/tr><tr><td>Longueur du secret<\/td><td>Variable<\/td><td>160 bits (20 octets)<\/td><\/tr><tr><td>Fen\u00eatre de tol\u00e9rance<\/td><td>0<\/td><td>\u00b11 pas (window=1)<\/td><\/tr><tr><td>Encodage du secret<\/td><td>Base32 (RFC 4648)<\/td><td>Base32<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Note importante sur SHA-1 : son usage dans HMAC reste s\u00fbr pour le TOTP. Les attaques par collision qui ont condamn\u00e9 SHA-1 pour les signatures, comme l&#8217;a montr\u00e9 l&#8217;<a href=\"\/fr\/collision-sha1\/\">attaque SHAttered<\/a>, ne s&#8217;appliquent pas \u00e0 HMAC, qui repose sur la r\u00e9sistance \u00e0 la pr\u00e9image et sur un secret. Vous pouvez donc garder SHA-1 sans crainte pour la 2FA, ce qui maximise la compatibilit\u00e9.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-1-et-2-initialiser-le-projet-et-installer-les-dependances\">\u00c9tape 1 et 2 : initialiser le projet et installer les d\u00e9pendances<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez un dossier de projet, initialisez un <code>package.json<\/code> et activez les modules ES. Nous utilisons la syntaxe d&#8217;import moderne, prise en charge nativement par Node 24. Le champ <code>\"type\": \"module\"<\/code> indique \u00e0 Node de traiter les fichiers <code>.js<\/code> comme des modules ES.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir totp-2fa-nodejs &amp;&amp; cd totp-2fa-nodejs\nnpm init -y\nnpm pkg set type=\"module\"\nnpm install otplib@13.4.1 qrcode@1.5.4 express@5.2.1 express-rate-limit@8.5.2 dotenv<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apr\u00e8s l&#8217;installation, v\u00e9rifiez que les paquets sont bien pr\u00e9sents. La commande ci-dessous liste les d\u00e9pendances de premier niveau avec leur version exacte. Conservez cette sortie : en cas de bug, comparer les versions install\u00e9es aux versions attendues est le premier r\u00e9flexe de d\u00e9pannage.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm ls --depth=0\n\n# Sortie attendue :\n# totp-2fa-nodejs@1.0.0\n# \u251c\u2500\u2500 dotenv@latest\n# \u251c\u2500\u2500 express@5.2.1\n# \u251c\u2500\u2500 express-rate-limit@8.5.2\n# \u251c\u2500\u2500 otplib@13.4.1\n# \u2514\u2500\u2500 qrcode@1.5.4<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez ensuite la structure de fichiers du projet. Nous s\u00e9parons la logique TOTP (le c\u0153ur cryptographique) de la couche HTTP (les routes Express) et du stockage. Cette s\u00e9paration facilite les tests et le remplacement ult\u00e9rieur du stockage en m\u00e9moire par une vraie base de donn\u00e9es.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>totp-2fa-nodejs\/\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 totp.js        # g\u00e9n\u00e9ration et v\u00e9rification TOTP\n\u2502   \u251c\u2500\u2500 crypto.js      # chiffrement du secret au repos\n\u2502   \u251c\u2500\u2500 recovery.js    # codes de r\u00e9cup\u00e9ration\n\u2502   \u251c\u2500\u2500 limites.js     # limitation et verrouillage\n\u2502   \u251c\u2500\u2500 store.js       # stockage (en m\u00e9moire pour la d\u00e9mo)\n\u2502   \u2514\u2500\u2500 server.js      # API Express\n\u2514\u2500\u2500 .env               # cl\u00e9 de chiffrement (jamais commit\u00e9e)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-3-generer-un-secret-totp-robuste\">\u00c9tape 3 : g\u00e9n\u00e9rer un secret TOTP robuste<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le secret est la pierre angulaire de la s\u00e9curit\u00e9. Il doit \u00eatre g\u00e9n\u00e9r\u00e9 avec un g\u00e9n\u00e9rateur cryptographiquement s\u00fbr, jamais avec <code>Math.random()<\/code>. otplib expose <code>authenticator.generateSecret()<\/code> qui produit un secret Base32 de longueur appropri\u00e9e. Par d\u00e9faut, otplib g\u00e9n\u00e8re un secret suffisant ; vous pouvez augmenter sa longueur pour atteindre les 160 bits recommand\u00e9s.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez le fichier <code>src\/totp.js<\/code>. Nous configurons explicitement otplib avec les param\u00e8tres standard et une fen\u00eatre de tol\u00e9rance de 1 pas, ce qui autorise un d\u00e9calage d&#8217;horloge de plus ou moins 30 secondes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/totp.js\nimport { authenticator } from 'otplib';\nimport QRCode from 'qrcode';\n\n\/\/ Configuration conforme aux param\u00e8tres interop\u00e9rables.\nauthenticator.options = {\n  digits: 6,        \/\/ code \u00e0 6 chiffres\n  step: 30,         \/\/ pas de temps de 30 secondes\n  window: 1,        \/\/ tol\u00e9rance de +\/- 1 pas (90 s au total)\n  algorithm: 'sha1' \/\/ compatibilit\u00e9 maximale\n};\n\nconst EMETTEUR = 'MonApp';\n\n\/\/ G\u00e9n\u00e8re un nouveau secret Base32 pour un utilisateur.\nexport function genererSecret() {\n  return authenticator.generateSecret(20); \/\/ 20 octets = 160 bits\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Testez la g\u00e9n\u00e9ration depuis un fichier temporaire ou le REPL Node. Chaque appel produit un secret diff\u00e9rent, une cha\u00eene Base32 d&#8217;environ 32 caract\u00e8res compos\u00e9e de lettres majuscules et de chiffres de 2 \u00e0 7. Ce secret ne doit jamais transiter en clair en dehors du QR code initial et ne doit jamais \u00eatre journalis\u00e9.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --input-type=module -e \"import('.\/src\/totp.js').then(m =&gt; console.log(m.genererSecret()))\"\n\n# Sortie (exemple) :\n# KRSXG5CTMVRXEZLUKNSWG4TFOQ2GS4ZA<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-4-creer-luri-otpauth-et-le-qr-code\">\u00c9tape 4 : cr\u00e9er l&#8217;URI otpauth:\/\/ et le QR code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pour enr\u00f4ler un utilisateur, l&#8217;application d&#8217;authentification doit importer le secret. La m\u00e9thode standard est l&#8217;URI de provisionnement <code>otpauth:\/\/<\/code>, encod\u00e9e dans un QR code que l&#8217;utilisateur scanne. Cette URI suit un format pr\u00e9cis d\u00e9fini par la documentation Key URI de Google, repris par tous les clients. Le tableau suivant d\u00e9taille chaque champ.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Champ<\/th><th>Exemple<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td>type<\/td><td>totp<\/td><td>Type d&#8217;OTP (totp ou hotp)<\/td><\/tr><tr><td>label<\/td><td>MonApp:alice@exemple.fr<\/td><td>\u00c9metteur et identifiant du compte<\/td><\/tr><tr><td>secret<\/td><td>KRSXG5CT&#8230;<\/td><td>Le secret Base32<\/td><\/tr><tr><td>issuer<\/td><td>MonApp<\/td><td>Nom du service affich\u00e9 dans l&#8217;application<\/td><\/tr><tr><td>algorithm<\/td><td>SHA1<\/td><td>Algorithme de hachage<\/td><\/tr><tr><td>digits<\/td><td>6<\/td><td>Longueur du code<\/td><\/tr><tr><td>period<\/td><td>30<\/td><td>Pas de temps en secondes<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">otplib construit cette URI avec <code>authenticator.keyuri(compte, emetteur, secret)<\/code>. Le paquet <code>qrcode<\/code> la transforme ensuite en image. Ajoutez ces fonctions \u00e0 <code>src\/totp.js<\/code>. La fonction <code>genererQrCode<\/code> retourne un data URI PNG en base64, directement utilisable dans l&#8217;attribut <code>src<\/code> d&#8217;une balise image c\u00f4t\u00e9 navigateur.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/totp.js (suite)\n\n\/\/ Construit l'URI otpauth:\/\/ standard.\nexport function genererUri(compte, secret) {\n  return authenticator.keyuri(compte, EMETTEUR, secret);\n}\n\n\/\/ Transforme l'URI en data URI PNG pour affichage.\nexport async function genererQrCode(compte, secret) {\n  const uri = genererUri(compte, secret);\n  return QRCode.toDataURL(uri, { width: 240, margin: 2 });\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;URI g\u00e9n\u00e9r\u00e9e ressemble \u00e0 <code>otpauth:\/\/totp\/MonApp:alice@exemple.fr?secret=KRSXG5CT...&amp;issuer=MonApp&amp;algorithm=SHA1&amp;digits=6&amp;period=30<\/code>. Proposez toujours une saisie manuelle du secret en compl\u00e9ment du QR code : certains utilisateurs scannent depuis le m\u00eame appareil que celui qui affiche le code et ne peuvent pas utiliser l&#8217;appareil photo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-5-verifier-un-code-totp-avec-tolerance-dhorloge\">\u00c9tape 5 : v\u00e9rifier un code TOTP avec tol\u00e9rance d&#8217;horloge<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La v\u00e9rification est le moment critique. otplib fournit <code>authenticator.verify({ token, secret })<\/code> qui renvoie un bool\u00e9en. Gr\u00e2ce \u00e0 l&#8217;option <code>window: 1<\/code> d\u00e9finie plus haut, la fonction accepte le code du pas courant, du pas pr\u00e9c\u00e9dent et du pas suivant. Important : otplib effectue une comparaison \u00e0 temps constant en interne pour \u00e9viter les attaques temporelles, vous n&#8217;avez pas \u00e0 g\u00e9rer cela vous-m\u00eame.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/totp.js (suite)\n\n\/\/ V\u00e9rifie un code saisi par l'utilisateur.\n\/\/ Retourne le d\u00e9calage (delta) du pas valid\u00e9, ou null si invalide.\nexport function verifierCode(token, secret) {\n  \/\/ Nettoyage : retirer les espaces que certaines applications ajoutent.\n  const propre = String(token).replace(\/\\s+\/g, '');\n  if (!\/^\\d{6}$\/.test(propre)) return null;\n\n  const valide = authenticator.verify({ token: propre, secret });\n  if (!valide) return null;\n\n  \/\/ checkDelta renvoie le d\u00e9calage (-1, 0, +1) du pas accept\u00e9.\n  return authenticator.checkDelta({ token: propre, secret });\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pourquoi retourner le delta plut\u00f4t qu&#8217;un simple bool\u00e9en ? Parce qu&#8217;il sert \u00e0 la protection contre le rejeu (\u00e9tape 9). En m\u00e9morisant le dernier pas de temps accept\u00e9, on peut refuser un code d\u00e9j\u00e0 utilis\u00e9 m\u00eame s&#8217;il est encore valide dans la fen\u00eatre. Voici un test rapide qui g\u00e9n\u00e8re un code \u00e0 la vol\u00e9e et le v\u00e9rifie imm\u00e9diatement.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ test-verif.js\nimport { authenticator } from 'otplib';\nimport { genererSecret, verifierCode } from '.\/src\/totp.js';\n\nconst secret = genererSecret();\nconst code = authenticator.generate(secret); \/\/ simule l'application mobile\nconsole.log('Code courant :', code);\nconsole.log('Resultat     :', verifierCode(code, secret));   \/\/ 0 = pas courant\nconsole.log('Faux code    :', verifierCode('000000', secret)); \/\/ null\n\n\/\/ Sortie (exemple) :\n\/\/ Code courant : 482913\n\/\/ Resultat     : 0\n\/\/ Faux code    : null<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-6-chiffrer-le-secret-au-repos-avec-aes-256-gcm\">\u00c9tape 6 : chiffrer le secret au repos avec AES-256-GCM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Stocker le secret TOTP en clair dans votre base de donn\u00e9es est une faute grave. Un attaquant qui acc\u00e8de \u00e0 la base pourrait g\u00e9n\u00e9rer des codes valides pour tous les comptes. La RFC 6238 demande explicitement de prot\u00e9ger les cl\u00e9s contre tout acc\u00e8s non autoris\u00e9. La parade : chiffrer chaque secret avec AES-256-GCM avant le stockage, \u00e0 l&#8217;aide d&#8217;une cl\u00e9 ma\u00eetre conserv\u00e9e hors de la base (variable d&#8217;environnement, gestionnaire de secrets ou HSM).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez <code>src\/crypto.js<\/code>. Nous utilisons le module natif <code>node:crypto<\/code>, sans d\u00e9pendance externe. AES-256-GCM fournit \u00e0 la fois confidentialit\u00e9 et authenticit\u00e9 : le tag d&#8217;authentification d\u00e9tecte toute alt\u00e9ration du chiffr\u00e9. Chaque chiffrement utilise un vecteur d&#8217;initialisation (IV) al\u00e9atoire de 12 octets, jamais r\u00e9utilis\u00e9.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/crypto.js\nimport { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';\n\n\/\/ Cl\u00e9 ma\u00eetre de 32 octets (256 bits), en hexad\u00e9cimal dans l'environnement.\nconst CLE = Buffer.from(process.env.TOTP_ENC_KEY || '', 'hex');\nif (CLE.length !== 32) {\n  throw new Error('TOTP_ENC_KEY doit faire 32 octets (64 caracteres hex).');\n}\n\nexport function chiffrer(texte) {\n  const iv = randomBytes(12);\n  const cipher = createCipheriv('aes-256-gcm', CLE, iv);\n  const chiffre = Buffer.concat([cipher.update(texte, 'utf8'), cipher.final()]);\n  const tag = cipher.getAuthTag();\n  \/\/ Format stock\u00e9 : iv:tag:chiffre, le tout en base64.\n  return [iv, tag, chiffre].map(b =&gt; b.toString('base64')).join(':');\n}\n\nexport function dechiffrer(charge) {\n  const [iv, tag, chiffre] = charge.split(':').map(s =&gt; Buffer.from(s, 'base64'));\n  const decipher = createDecipheriv('aes-256-gcm', CLE, iv);\n  decipher.setAuthTag(tag);\n  return Buffer.concat([decipher.update(chiffre), decipher.final()]).toString('utf8');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">G\u00e9n\u00e9rez une cl\u00e9 ma\u00eetre robuste et placez-la dans un fichier <code>.env<\/code> exclu du d\u00e9p\u00f4t Git. Ne r\u00e9utilisez jamais cette cl\u00e9 entre environnements de d\u00e9veloppement et de production. Pour approfondir l&#8217;usage du module crypto, consultez notre guide complet sur le <a href=\"\/fr\/hmac-sha256-nodejs\/\">HMAC-SHA256 en Node.js<\/a>, qui partage les m\u00eames primitives.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># G\u00e9n\u00e9rer une cl\u00e9 de 256 bits et l'\u00e9crire dans .env\nnode -e \"console.log('TOTP_ENC_KEY=' + require('crypto').randomBytes(32).toString('hex'))\" &gt; .env\n\n# V\u00e9rifier (64 caract\u00e8res hexad\u00e9cimaux apr\u00e8s le signe =)\ncat .env<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-7-generer-des-codes-de-recuperation\">\u00c9tape 7 : g\u00e9n\u00e9rer des codes de r\u00e9cup\u00e9ration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Que se passe-t-il si l&#8217;utilisateur perd son t\u00e9l\u00e9phone ? Sans plan de secours, il est d\u00e9finitivement bloqu\u00e9. La solution standard est de fournir, au moment de l&#8217;activation, une liste de codes de r\u00e9cup\u00e9ration \u00e0 usage unique. L&#8217;utilisateur les imprime ou les stocke dans son gestionnaire de mots de passe. Chaque code ne fonctionne qu&#8217;une fois et contourne le TOTP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Point essentiel : ne stockez jamais ces codes en clair. Traitez-les comme des mots de passe et hachez-les. Comme ils sont \u00e0 forte entropie (g\u00e9n\u00e9r\u00e9s al\u00e9atoirement), un hachage SHA-256 suffit ; inutile d&#8217;utiliser un algorithme lent comme Argon2, r\u00e9serv\u00e9 aux mots de passe humains \u00e0 faible entropie. Cr\u00e9ez <code>src\/recovery.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/recovery.js\nimport { randomBytes, createHash, timingSafeEqual } from 'node:crypto';\n\n\/\/ G\u00e9n\u00e8re 10 codes lisibles de type XXXX-XXXX.\nexport function genererCodesRecuperation(nombre = 10) {\n  const codes = [];\n  for (let i = 0; i &lt; nombre; i++) {\n    const brut = randomBytes(4).toString('hex').toUpperCase(); \/\/ 8 caract\u00e8res\n    codes.push(brut.slice(0, 4) + '-' + brut.slice(4));\n  }\n  return codes;\n}\n\nexport function hacherCode(code) {\n  const normalise = code.replace(\/-\/g, '').toUpperCase();\n  return createHash('sha256').update(normalise).digest('hex');\n}\n\n\/\/ Compare en temps constant et renvoie le hachage trouv\u00e9 ou null.\nexport function verifierCodeRecuperation(saisie, hashesStockes) {\n  const hashSaisie = Buffer.from(hacherCode(saisie), 'hex');\n  return hashesStockes.find(h =&gt; {\n    const ref = Buffer.from(h, 'hex');\n    return ref.length === hashSaisie.length &amp;&amp; timingSafeEqual(ref, hashSaisie);\n  }) ?? null;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lorsqu&#8217;un code de r\u00e9cup\u00e9ration est utilis\u00e9 avec succ\u00e8s, supprimez son hachage de la liste stock\u00e9e pour emp\u00eacher toute r\u00e9utilisation. Pr\u00e9venez l&#8217;utilisateur quand il ne lui reste plus que deux ou trois codes, afin qu&#8217;il en r\u00e9g\u00e9n\u00e8re un nouveau lot. Cette logique de secrets \u00e0 usage unique compl\u00e8te bien notre tutoriel sur le <a href=\"\/fr\/bcrypt-nodejs-hachage-mot-de-passe\/\">hachage de mots de passe avec bcrypt<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-8-limiter-les-tentatives-de-verification\">\u00c9tape 8 : limiter les tentatives de v\u00e9rification<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un code \u00e0 6 chiffres n&#8217;offre qu&#8217;un million de combinaisons. Avec la fen\u00eatre de tol\u00e9rance, un attaquant disposant d&#8217;un d\u00e9bit illimit\u00e9 finirait par deviner un code valide. La RFC 4226 insiste sur la n\u00e9cessit\u00e9 de limiter les tentatives. Nous appliquons une double protection : un verrouillage par compte apr\u00e8s plusieurs \u00e9checs, et une limitation globale par adresse IP avec <code>express-rate-limit<\/code>. Cr\u00e9ez <code>src\/limites.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/limites.js\nimport rateLimit from 'express-rate-limit';\n\n\/\/ Limiteur d\u00e9di\u00e9 \u00e0 la route de v\u00e9rification 2FA.\nexport const limiteurVerif = rateLimit({\n  windowMs: 15 * 60 * 1000, \/\/ fen\u00eatre de 15 minutes\n  max: 5,                   \/\/ 5 tentatives par IP et par fen\u00eatre\n  standardHeaders: true,\n  legacyHeaders: false,\n  message: { erreur: 'Trop de tentatives. R\u00e9essayez dans 15 minutes.' }\n});\n\nconst MAX_ECHECS = 5;\n\nexport function compteVerrouille(u) {\n  return Boolean(u.verrouJusqua) &amp;&amp; Date.now() &lt; u.verrouJusqua;\n}\n\nexport function enregistrerEchec(u) {\n  u.echecs = (u.echecs || 0) + 1;\n  if (u.echecs &gt;= MAX_ECHECS) {\n    u.verrouJusqua = Date.now() + 15 * 60 * 1000;\n    u.echecs = 0;\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La limitation par IP ne suffit pas seule : un attaquant peut faire tourner les adresses. Le compteur d&#8217;\u00e9checs par compte ferme cette br\u00e8che. Apr\u00e8s 5 \u00e9checs cons\u00e9cutifs, le compte est verrouill\u00e9 pendant 15 minutes ; notifiez alors l&#8217;utilisateur par email. R\u00e9initialisez le compteur \u00e0 chaque succ\u00e8s. Ce m\u00e9canisme, combin\u00e9 \u00e0 la fen\u00eatre \u00e9troite, rend la force brute en ligne irr\u00e9aliste.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-9-proteger-contre-le-rejeu-de-code\">\u00c9tape 9 : prot\u00e9ger contre le rejeu de code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un code TOTP reste valide pendant 30 \u00e0 90 secondes selon la fen\u00eatre. Sans protection, un attaquant qui intercepte un code (par hame\u00e7onnage en temps r\u00e9el, par exemple) peut le rejouer tant qu&#8217;il n&#8217;a pas expir\u00e9. La RFC 6238 recommande d&#8217;accepter chaque code au plus une fois. La technique : stocker le dernier pas de temps valid\u00e9 par utilisateur et refuser tout code dont le pas est inf\u00e9rieur ou \u00e9gal au dernier accept\u00e9.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/totp.js (suite) : v\u00e9rification sans rejeu\nexport function verifierSansRejeu(token, secret, utilisateur) {\n  const delta = verifierCode(token, secret);\n  if (delta === null) return false;\n\n  \/\/ Pas de temps absolu du code accept\u00e9.\n  const pasActuel = Math.floor(Date.now() \/ 1000 \/ 30) + delta;\n\n  if (utilisateur.dernierPas &amp;&amp; pasActuel &lt;= utilisateur.dernierPas) {\n    return false; \/\/ code d\u00e9j\u00e0 utilis\u00e9 : rejet\n  }\n  utilisateur.dernierPas = pasActuel;\n  return true;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cette protection est particuli\u00e8rement importante face aux kits de hame\u00e7onnage modernes, qui relaient les identifiants en temps r\u00e9el. Elle ne remplace pas une vraie r\u00e9sistance au hame\u00e7onnage (que seules les passkeys offrent), mais elle ferme une fen\u00eatre d&#8217;attaque concr\u00e8te. Pour comprendre comment ces attaques fonctionnent, lisez notre dossier sur les <a href=\"\/fr\/hameconnage\/\">attaques par hame\u00e7onnage<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-10-a-12-assembler-lapi-express-complete\">\u00c9tape 10 \u00e0 12 : assembler l&#8217;API Express compl\u00e8te<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nous r\u00e9unissons maintenant tous les modules dans un serveur Express. Trois routes structurent le parcours : <code>\/2fa\/setup<\/code> g\u00e9n\u00e8re le secret et le QR code, <code>\/2fa\/activate<\/code> confirme l&#8217;activation apr\u00e8s un premier code valide, et <code>\/2fa\/verify<\/code> valide un code lors de la connexion. En production, ces routes seraient prot\u00e9g\u00e9es par une session authentifi\u00e9e (mot de passe d\u00e9j\u00e0 v\u00e9rifi\u00e9). Pour la d\u00e9monstration, nous simulons un seul utilisateur.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/store.js : stockage en m\u00e9moire pour la d\u00e9mo.\n\/\/ En production, remplacez par PostgreSQL, MySQL ou autre.\nconst utilisateurs = new Map();\n\nexport function getUtilisateur(id) {\n  if (!utilisateurs.has(id)) {\n    utilisateurs.set(id, {\n      id, secretChiffre: null, actif: false,\n      codesRecup: [], echecs: 0, dernierPas: 0\n    });\n  }\n  return utilisateurs.get(id);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Voici le serveur complet. Notez l&#8217;ordre logique : on ne marque jamais la 2FA comme active tant que l&#8217;utilisateur n&#8217;a pas prouv\u00e9 qu&#8217;il peut g\u00e9n\u00e9rer un code valide. Cela \u00e9vite de verrouiller un utilisateur qui aurait mal scann\u00e9 le QR code.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/server.js\nimport 'dotenv\/config';\nimport express from 'express';\nimport { genererSecret, genererQrCode, verifierSansRejeu, verifierCode } from '.\/totp.js';\nimport { chiffrer, dechiffrer } from '.\/crypto.js';\nimport { genererCodesRecuperation, hacherCode } from '.\/recovery.js';\nimport { getUtilisateur } from '.\/store.js';\nimport { limiteurVerif, compteVerrouille, enregistrerEchec } from '.\/limites.js';\n\nconst app = express();\napp.use(express.json());\n\n\/\/ \u00c9tape 10 : initialiser l'enr\u00f4lement.\napp.post('\/2fa\/setup', async (req, res) =&gt; {\n  const u = getUtilisateur(req.body.userId || 'demo');\n  const secret = genererSecret();\n  u.secretChiffre = chiffrer(secret); \/\/ chiffr\u00e9 au repos\n  const qr = await genererQrCode(u.id + '@exemple.fr', secret);\n  res.json({ qrCode: qr, secretManuel: secret });\n});\n\n\/\/ \u00c9tape 11 : activer apr\u00e8s v\u00e9rification d'un premier code.\napp.post('\/2fa\/activate', (req, res) =&gt; {\n  const u = getUtilisateur(req.body.userId || 'demo');\n  if (!u.secretChiffre) return res.status(400).json({ erreur: 'Aucun enrolement.' });\n  const secret = dechiffrer(u.secretChiffre);\n  if (verifierCode(req.body.code, secret) === null) {\n    return res.status(400).json({ erreur: 'Code invalide.' });\n  }\n  const codes = genererCodesRecuperation();\n  u.codesRecup = codes.map(hacherCode);\n  u.actif = true;\n  res.json({ message: '2FA activ\u00e9e.', codesRecuperation: codes });\n});\n\n\/\/ \u00c9tape 12 : v\u00e9rifier lors de la connexion.\napp.post('\/2fa\/verify', limiteurVerif, (req, res) =&gt; {\n  const u = getUtilisateur(req.body.userId || 'demo');\n  if (!u.actif) return res.status(400).json({ erreur: '2FA non activ\u00e9e.' });\n  if (compteVerrouille(u)) return res.status(429).json({ erreur: 'Compte verrouill\u00e9.' });\n  const secret = dechiffrer(u.secretChiffre);\n  if (!verifierSansRejeu(req.body.code, secret, u)) {\n    enregistrerEchec(u);\n    return res.status(401).json({ erreur: 'Code incorrect.' });\n  }\n  u.echecs = 0;\n  res.json({ message: 'Authentification r\u00e9ussie.' });\n});\n\napp.listen(3000, () =&gt; console.log('Serveur 2FA sur http:\/\/localhost:3000'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lancez le serveur avec <code>node src\/server.js<\/code>, puis testez le parcours complet avec curl. La s\u00e9quence ci-dessous enr\u00f4le l&#8217;utilisateur, affiche le secret manuel (\u00e0 entrer dans votre application d&#8217;authentification), puis active et v\u00e9rifie la 2FA. Remplacez 482913 par le code r\u00e9el affich\u00e9 dans l&#8217;application.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># 1. Enr\u00f4lement : r\u00e9cup\u00e8re le QR code et le secret manuel\ncurl -s -X POST http:\/\/localhost:3000\/2fa\/setup \\\n  -H 'Content-Type: application\/json' -d '{\"userId\":\"demo\"}'\n\n# 2. Activation avec le code affich\u00e9 dans l'application\ncurl -s -X POST http:\/\/localhost:3000\/2fa\/activate \\\n  -H 'Content-Type: application\/json' -d '{\"userId\":\"demo\",\"code\":\"482913\"}'\n\n# Sortie attendue :\n# {\"message\":\"2FA activ\u00e9e.\",\"codesRecuperation\":[\"A1B2-C3D4\", ...]}\n\n# 3. V\u00e9rification \u00e0 la connexion\ncurl -s -X POST http:\/\/localhost:3000\/2fa\/verify \\\n  -H 'Content-Type: application\/json' -d '{\"userId\":\"demo\",\"code\":\"715092\"}'\n\n# Sortie attendue :\n# {\"message\":\"Authentification r\u00e9ussie.\"}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"le-projet-complet-recapitulatif-des-fichiers\">Le projet complet : r\u00e9capitulatif des fichiers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Vous disposez d\u00e9sormais d&#8217;un syst\u00e8me 2FA fonctionnel et modulaire. Chaque responsabilit\u00e9 vit dans son fichier : g\u00e9n\u00e9ration et v\u00e9rification dans <code>totp.js<\/code>, chiffrement dans <code>crypto.js<\/code>, codes de secours dans <code>recovery.js<\/code>, politique anti-abus dans <code>limites.js<\/code>, stockage dans <code>store.js<\/code> et exposition HTTP dans <code>server.js<\/code>. Cette architecture en couches rend chaque morceau testable isol\u00e9ment et facilite l&#8217;audit de s\u00e9curit\u00e9.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour passer en production, trois changements suffisent. Remplacez le <code>Map<\/code> en m\u00e9moire de <code>store.js<\/code> par des requ\u00eates vers votre base de donn\u00e9es, en stockant la colonne <code>secretChiffre<\/code> (jamais le secret en clair), le tableau de hachages de codes de r\u00e9cup\u00e9ration et le champ <code>dernierPas<\/code>. Liez ensuite chaque route \u00e0 votre middleware de session existant pour exiger une authentification primaire avant toute op\u00e9ration 2FA. Enfin, d\u00e9placez la cl\u00e9 <code>TOTP_ENC_KEY<\/code> vers un gestionnaire de secrets (Vault, AWS KMS, ou \u00e9quivalent) plut\u00f4t qu&#8217;un simple fichier <code>.env<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le flux utilisateur final se r\u00e9sume ainsi : l&#8217;utilisateur active la 2FA depuis ses param\u00e8tres (setup puis activate), re\u00e7oit ses codes de r\u00e9cup\u00e9ration une seule fois, puis, \u00e0 chaque connexion ult\u00e9rieure, saisit le code de son application apr\u00e8s son mot de passe. Le serveur d\u00e9chiffre le secret, v\u00e9rifie le code avec tol\u00e9rance, bloque le rejeu, compte les \u00e9checs et journalise l&#8217;\u00e9v\u00e8nement. C&#8217;est exactement le comportement attendu d&#8217;une 2FA de niveau professionnel.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cinq-erreurs-frequentes-a-eviter\">Cinq erreurs fr\u00e9quentes \u00e0 \u00e9viter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ces pi\u00e8ges reviennent dans presque toutes les impl\u00e9mentations TOTP de d\u00e9butants. Les conna\u00eetre \u00e0 l&#8217;avance vous fera gagner des heures de d\u00e9bogage.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Stocker le secret en clair.<\/strong> C&#8217;est l&#8217;erreur la plus dangereuse. Un vol de base compromet alors tous les comptes. Chiffrez syst\u00e9matiquement le secret avec AES-256-GCM (\u00e9tape 6) et gardez la cl\u00e9 ma\u00eetre hors de la base.<\/li><li><strong>Oublier la fen\u00eatre de tol\u00e9rance.<\/strong> Avec <code>window: 0<\/code>, le moindre d\u00e9calage d&#8217;horloge fait \u00e9chouer la v\u00e9rification, ce qui g\u00e9n\u00e8re un flot de tickets de support. Utilisez <code>window: 1<\/code> sans d\u00e9passer <code>2<\/code>.<\/li><li><strong>Activer la 2FA avant validation.<\/strong> Si vous marquez le compte comme prot\u00e9g\u00e9 d\u00e8s la g\u00e9n\u00e9ration du secret, un utilisateur qui scanne mal le QR code se retrouve enferm\u00e9 dehors. Exigez toujours un premier code valide avant d&#8217;activer.<\/li><li><strong>Ne pas nettoyer la saisie.<\/strong> Les applications affichent souvent le code sous la forme \u00ab 482 913 \u00bb. Si vous ne retirez pas les espaces, la v\u00e9rification \u00e9choue alors que le code est bon. Filtrez avec une expression r\u00e9guli\u00e8re.<\/li><li><strong>Personnaliser l&#8217;algorithme sans raison.<\/strong> Passer en SHA-256 ou \u00e0 8 chiffres casse la compatibilit\u00e9 avec d&#8217;anciennes versions de Google Authenticator, qui ignorent ces param\u00e8tres et calculent un code SHA-1 \u00e0 6 chiffres. Restez sur les valeurs par d\u00e9faut.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"depannage-huit-problemes-courants-et-leurs-solutions\">D\u00e9pannage : huit probl\u00e8mes courants et leurs solutions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le tableau suivant recense les sympt\u00f4mes les plus signal\u00e9s lors de la mise en place du TOTP, avec leur cause probable et la correction. Gardez-le sous la main pendant vos tests.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Sympt\u00f4me<\/th><th>Cause probable<\/th><th>Solution<\/th><\/tr><\/thead><tbody><tr><td>Tous les codes sont refus\u00e9s<\/td><td>Horloge serveur d\u00e9synchronis\u00e9e<\/td><td>Synchroniser via NTP (chrony ou systemd-timesyncd)<\/td><\/tr><tr><td>Le code marche une fois sur deux<\/td><td>Fen\u00eatre de tol\u00e9rance \u00e0 0<\/td><td>D\u00e9finir window=1<\/td><\/tr><tr><td>Code refus\u00e9 avec espaces<\/td><td>Saisie non nettoy\u00e9e<\/td><td>Retirer les espaces avant v\u00e9rification<\/td><\/tr><tr><td>QR code illisible<\/td><td>\u00c9metteur ou label mal encod\u00e9<\/td><td>Encoder les caract\u00e8res sp\u00e9ciaux dans le label<\/td><\/tr><tr><td>L&#8217;application affiche le mauvais nom<\/td><td>Champ issuer manquant<\/td><td>Passer l&#8217;\u00e9metteur \u00e0 keyuri()<\/td><\/tr><tr><td>Erreur TOTP_ENC_KEY length<\/td><td>Cl\u00e9 ma\u00eetre absente ou mauvaise taille<\/td><td>R\u00e9g\u00e9n\u00e9rer 32 octets (64 hex) dans .env<\/td><\/tr><tr><td>Codes SHA-256 refus\u00e9s sur mobile<\/td><td>Application ne g\u00e8re que SHA-1<\/td><td>Revenir \u00e0 SHA-1 pour la compatibilit\u00e9<\/td><\/tr><tr><td>Rejet apr\u00e8s changement d&#8217;heure<\/td><td>Confusion heure locale \/ UTC<\/td><td>TOTP utilise l&#8217;heure Unix UTC, v\u00e9rifier le fuseau serveur<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">La cause num\u00e9ro un, de loin, est la d\u00e9rive d&#8217;horloge. Le TOTP repose enti\u00e8rement sur une heure exacte c\u00f4t\u00e9 serveur. Sur un serveur Linux, v\u00e9rifiez la synchronisation avec <code>timedatectl status<\/code> et assurez-vous que la ligne \u00ab System clock synchronized \u00bb indique \u00ab yes \u00bb. Sans NTP actif, votre serveur d\u00e9rivera de quelques secondes par jour et finira par rejeter des codes pourtant corrects.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># V\u00e9rifier la synchronisation de l'horloge sur Linux\ntimedatectl status\n\n# Sortie souhait\u00e9e :\n#   System clock synchronized: yes\n#                 NTP service: active<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"astuces-avancees-pour-une-2fa-de-qualite-production\">Astuces avanc\u00e9es pour une 2FA de qualit\u00e9 production<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Une fois le socle en place, plusieurs am\u00e9liorations distinguent une d\u00e9monstration d&#8217;un syst\u00e8me pr\u00eat pour la production. La premi\u00e8re concerne la rotation des cl\u00e9s de chiffrement. Votre cl\u00e9 ma\u00eetre AES doit pouvoir changer sans invalider les secrets existants. Impl\u00e9mentez un identifiant de version de cl\u00e9 stock\u00e9 \u00e0 c\u00f4t\u00e9 de chaque secret chiffr\u00e9, afin de d\u00e9chiffrer avec l&#8217;ancienne cl\u00e9 puis de rechiffrer avec la nouvelle lors d&#8217;une migration progressive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Deuxi\u00e8me axe, l&#8217;enr\u00f4lement de plusieurs appareils. Certains utilisateurs veulent enregistrer le m\u00eame compte sur leur t\u00e9l\u00e9phone et leur tablette. Comme le secret est partag\u00e9, il suffit de leur pr\u00e9senter le m\u00eame QR code lors de l&#8217;activation. En revanche, ne g\u00e9n\u00e9rez jamais plusieurs secrets distincts pour un m\u00eame compte sans logique de gestion claire, sous peine de validations incoh\u00e9rentes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Troisi\u00e8me axe, la journalisation de s\u00e9curit\u00e9. Enregistrez chaque tentative de v\u00e9rification 2FA (succ\u00e8s et \u00e9chec) avec l&#8217;horodatage et l&#8217;adresse IP, sans jamais journaliser le code ni le secret. Ces journaux alimentent la d\u00e9tection d&#8217;anomalies : une rafale d&#8217;\u00e9checs depuis plusieurs pays signale une attaque. Couplez-les \u00e0 des alertes par email vers l&#8217;utilisateur lors d&#8217;un nouvel appareil ou d&#8217;un verrouillage de compte.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quatri\u00e8me axe, la conformit\u00e9. Les recommandations du <a href=\"https:\/\/pages.nist.gov\/800-63-3\/sp800-63b.html\" target=\"_blank\" rel=\"noopener nofollow\">NIST SP 800-63B<\/a> classent le TOTP comme authentificateur \u00ab OTP multi-facteur \u00bb acceptable au niveau d&#8217;assurance AAL2, \u00e0 condition de prot\u00e9ger le secret et de limiter les tentatives. L&#8217;<a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Multifactor_Authentication_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener nofollow\">aide-m\u00e9moire MFA de l&#8217;OWASP<\/a> d\u00e9taille les contr\u00f4les compl\u00e9mentaires \u00e0 mettre en place. Enfin, pour les comptes \u00e0 tr\u00e8s haut risque, proposez en plus une cl\u00e9 mat\u00e9rielle FIDO2, plus robuste face au hame\u00e7onnage que le TOTP.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"totp-hotp-et-passkeys-que-choisir\">TOTP, HOTP et passkeys : que choisir<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le TOTP n&#8217;est pas la seule option, et bien choisir d\u00e9pend de votre contexte. Le HOTP (RFC 4226), bas\u00e9 sur un compteur d&#8217;\u00e9v\u00e8nements plut\u00f4t que sur le temps, sert surtout aux jetons mat\u00e9riels sans horloge interne. Il pose un probl\u00e8me de d\u00e9synchronisation du compteur si l&#8217;utilisateur g\u00e9n\u00e8re des codes sans les valider, ce qui le rend moins pratique pour le web. Le TOTP, lui, se resynchronise \u00e0 chaque tic d&#8217;horloge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les passkeys repr\u00e9sentent la g\u00e9n\u00e9ration suivante. Fond\u00e9es sur la cryptographie \u00e0 cl\u00e9 publique (WebAuthn et FIDO2), elles r\u00e9sistent au hame\u00e7onnage par conception, car la signature est li\u00e9e au domaine du site. La strat\u00e9gie gagnante en 2026 consiste \u00e0 proposer les passkeys comme m\u00e9thode principale et le TOTP comme repli universel, car tous les utilisateurs n&#8217;ont pas encore d&#8217;appareil compatible passkey. Pour aller plus loin sur les fondamentaux des signatures \u00e0 cl\u00e9 publique, lisez notre dossier sur les <a href=\"\/fr\/signatures-numeriques\/\">signatures num\u00e9riques<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Crit\u00e8re<\/th><th>HOTP<\/th><th>TOTP<\/th><th>Passkey (FIDO2)<\/th><\/tr><\/thead><tbody><tr><td>Base<\/td><td>Compteur<\/td><td>Temps<\/td><td>Cl\u00e9 publique<\/td><\/tr><tr><td>RFC \/ standard<\/td><td>RFC 4226<\/td><td>RFC 6238<\/td><td>WebAuthn<\/td><\/tr><tr><td>R\u00e9sistance au hame\u00e7onnage<\/td><td>Non<\/td><td>Non<\/td><td>Oui<\/td><\/tr><tr><td>Risque de d\u00e9synchronisation<\/td><td>\u00c9lev\u00e9<\/td><td>Faible<\/td><td>Aucun<\/td><\/tr><tr><td>D\u00e9pend d&#8217;une horloge<\/td><td>Non<\/td><td>Oui<\/td><td>Non<\/td><\/tr><tr><td>Compatibilit\u00e9 applications<\/td><td>Limit\u00e9e<\/td><td>Universelle<\/td><td>Croissante<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"questions-frequentes-sur-le-totp-en-node-js\">Questions fr\u00e9quentes sur le TOTP en Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faut-il-utiliser-otplib-ou-speakeasy\">Faut-il utiliser otplib ou speakeasy ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Utilisez otplib (version 13.4.1 en juin 2026). La biblioth\u00e8que speakeasy n&#8217;a pas re\u00e7u de mise \u00e0 jour depuis sa version 2.0.0 de 2016. otplib est activement maintenue, propose un typage moderne et une API claire pour le TOTP, le HOTP et les URI de provisionnement. Pour un nouveau projet, le choix est sans ambigu\u00eft\u00e9.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"le-totp-en-sha-1-est-il-sur-en-2026\">Le TOTP en SHA-1 est-il s\u00fbr en 2026 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Oui. Les faiblesses de SHA-1 concernent la r\u00e9sistance aux collisions, exploit\u00e9e pour falsifier des signatures. Le TOTP utilise HMAC-SHA1, qui repose sur un secret et sur la r\u00e9sistance \u00e0 la pr\u00e9image, non affect\u00e9e par ces attaques. Conserver SHA-1 maximise la compatibilit\u00e9 sans compromettre la s\u00e9curit\u00e9 de la 2FA.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"combien-de-codes-de-recuperation-generer\">Combien de codes de r\u00e9cup\u00e9ration g\u00e9n\u00e9rer ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dix codes \u00e0 usage unique constituent une norme confortable. Stockez-en uniquement les hachages SHA-256, supprimez chaque code apr\u00e8s usage et invitez l&#8217;utilisateur \u00e0 r\u00e9g\u00e9n\u00e9rer un lot quand il en reste moins de trois. Affichez-les une seule fois, au moment de l&#8217;activation, jamais ensuite.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"que-faire-si-lutilisateur-perd-son-telephone-et-ses-codes\">Que faire si l&#8217;utilisateur perd son t\u00e9l\u00e9phone et ses codes ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pr\u00e9voyez une proc\u00e9dure de r\u00e9cup\u00e9ration de compte distincte, par exemple une v\u00e9rification d&#8217;identit\u00e9 renforc\u00e9e par le support, suivie d&#8217;une r\u00e9initialisation de la 2FA. Cette proc\u00e9dure doit \u00eatre plus stricte qu&#8217;une simple connexion, car elle constitue la porte de secours et donc une cible privil\u00e9gi\u00e9e pour l&#8217;ing\u00e9nierie sociale.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"le-totp-protege-t-il-contre-le-hameconnage\">Le TOTP prot\u00e8ge-t-il contre le hame\u00e7onnage ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Partiellement. La protection anti-rejeu (\u00e9tape 9) emp\u00eache la r\u00e9utilisation d&#8217;un code intercept\u00e9, mais un kit de hame\u00e7onnage en temps r\u00e9el peut relayer le code dans sa fen\u00eatre de validit\u00e9. Seules les passkeys, li\u00e9es au domaine, offrent une vraie r\u00e9sistance au hame\u00e7onnage. Le TOTP reste n\u00e9anmoins tr\u00e8s sup\u00e9rieur au mot de passe seul.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"peut-on-tester-le-totp-sans-application-mobile\">Peut-on tester le TOTP sans application mobile ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Oui. La fonction <code>authenticator.generate(secret)<\/code> d&#8217;otplib calcule le code courant c\u00f4t\u00e9 serveur, ce qui permet de scripter des tests automatis\u00e9s. Vous pouvez ainsi valider votre flux complet en int\u00e9gration continue, sans intervention humaine ni appareil physique.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faut-il-chiffrer-le-secret-ou-le-hacher\">Faut-il chiffrer le secret ou le hacher ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Il faut le chiffrer, pas le hacher. Le serveur a besoin du secret en clair pour recalculer le code \u00e0 chaque v\u00e9rification, donc une op\u00e9ration r\u00e9versible (chiffrement AES-256-GCM) est obligatoire. Le hachage, irr\u00e9versible, conviendrait aux mots de passe mais pas aux secrets TOTP.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quelle-version-de-node-js-utiliser\">Quelle version de Node.js utiliser ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">La ligne LTS Node.js 24 (\u00ab Krypton \u00bb) est le choix recommand\u00e9 en 2026. Elle apporte un module <code>node:crypto<\/code> stable, la prise en charge native des modules ES et des correctifs de s\u00e9curit\u00e9 \u00e0 long terme. \u00c9vitez les versions impaires (non LTS) en production.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"related-coverage\">Related Coverage<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/fr\/authentification-jwt-nodejs\/\">Authentification JWT en Node.js : 12 \u00e9tapes<\/a><\/li><li><a href=\"\/fr\/application-authentification-2fa\/\">Applis 2FA : 7 test\u00e9es, Ente Auth en t\u00eate<\/a><\/li><li><a href=\"\/fr\/bcrypt-nodejs-hachage-mot-de-passe\/\">bcrypt Node.js : hacher un mot de passe en 12 \u00e9tapes<\/a><\/li><li><a href=\"\/fr\/oauth2-openid-connect-nodejs\/\">OAuth2 en Node.js : flux s\u00e9curis\u00e9 en 12 \u00e9tapes<\/a><\/li><li><a href=\"\/fr\/hmac-sha256-nodejs\/\">HMAC-SHA256 en Node.js : signer une API<\/a><\/li><li><a href=\"\/fr\/securite-des-mots-de-passe\/\">S\u00e9curit\u00e9 des mots de passe : ce qui prot\u00e8ge vraiment<\/a><\/li><li><a href=\"\/fr\/security-hub\/\">Toutes nos ressources sur la s\u00e9curit\u00e9<\/a><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Article publi\u00e9 le 14 juin 2026. Versions logicielles v\u00e9rifi\u00e9es sur le registre npm et le calendrier de publication Node.js \u00e0 cette date. Le code de ce tutoriel est fourni \u00e0 titre p\u00e9dagogique : auditez-le et adaptez-le \u00e0 votre contexte de production avant tout d\u00e9ploiement.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>L&#8217;authentification TOTP (Time-based One-Time Password) est devenue le second facteur de r\u00e9f\u00e9rence pour prot\u00e9ger les comptes en ligne. Contrairement au SMS, vuln\u00e9rable au SIM swapping, un code TOTP est calcul\u00e9\u2026<\/p>\n","protected":false},"author":2,"featured_media":157,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-156","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-10","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/156","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/comments?post=156"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/156\/revisions"}],"predecessor-version":[{"id":158,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/156\/revisions\/158"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media\/157"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media?parent=156"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/categories?post=156"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/tags?post=156"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}