{"id":61,"date":"2026-06-11T16:24:02","date_gmt":"2026-06-11T16:24:02","guid":{"rendered":"https:\/\/shattered.io\/fr\/2026\/06\/11\/authentification-jwt-nodejs\/"},"modified":"2026-06-11T16:24:02","modified_gmt":"2026-06-11T16:24:02","slug":"authentification-jwt-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/fr\/2026\/06\/11\/authentification-jwt-nodejs\/","title":{"rendered":"Authentification JWT en Node.js : 12 \u00e9tapes [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Le jeton JWT (JSON Web Token) est devenu le standard de fait pour l&#8217;authentification sans \u00e9tat dans les API modernes. En 2026, la biblioth\u00e8que <code>jsonwebtoken<\/code> reste la r\u00e9f\u00e9rence c\u00f4t\u00e9 Node.js avec sa version stable <strong>9.0.3<\/strong>, que Snyk classe sans vuln\u00e9rabilit\u00e9 connue. Ce tutoriel vous guide en 12 \u00e9tapes pour construire un syst\u00e8me d&#8217;authentification JWT complet, s\u00e9curis\u00e9 et pr\u00eat pour la production, avec <code>access token<\/code>, <code>refresh token<\/code>, hachage de mot de passe et middleware de v\u00e9rification.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Comptez environ 45 minutes pour le suivre de bout en bout. \u00c0 la fin, vous disposerez d&#8217;un projet Express fonctionnel, d&#8217;une compr\u00e9hension claire des algorithmes <code>HS256<\/code>, <code>RS256<\/code> et <code>ES256<\/code>, et d&#8217;une liste de pi\u00e8ges \u00e0 \u00e9viter qui font tomber la majorit\u00e9 des impl\u00e9mentations amateurs. Tout le code est test\u00e9 sur Node.js 22 LTS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"jwt-en-2026-pourquoi-lauthentification-par-jeton-domine\">JWT en 2026 : pourquoi l&#8217;authentification par jeton domine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un JWT transporte des informations v\u00e9rifiables entre deux parties sous forme d&#8217;objet JSON sign\u00e9. Contrairement aux sessions classiques, le serveur n&#8217;a rien \u00e0 stocker : le jeton lui-m\u00eame contient les revendications (claims) sur l&#8217;utilisateur, et la signature garantit qu&#8217;il n&#8217;a pas \u00e9t\u00e9 falsifi\u00e9. Cette nature <strong>sans \u00e9tat<\/strong> explique son succ\u00e8s dans les architectures distribu\u00e9es, les microservices et les applications mobiles, o\u00f9 conserver une session c\u00f4t\u00e9 serveur sur chaque n\u0153ud co\u00fbte cher.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La sp\u00e9cification officielle, la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7519\" target=\"_blank\" rel=\"noopener\">RFC 7519<\/a>, d\u00e9finit la structure du jeton. Une seconde RFC, la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8725\" target=\"_blank\" rel=\"noopener\">RFC 8725 (JWT Best Current Practices)<\/a>, publi\u00e9e par l&#8217;IETF, \u00e9num\u00e8re les contre-mesures contre les attaques connues. Lire ces deux documents change la fa\u00e7on dont vous \u00e9crivez votre code : on comprend vite que JWT n&#8217;est pas magique et qu&#8217;une mauvaise configuration ouvre des failles graves.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le d\u00e9bat JWT contre sessions revient sans cesse. Les sessions sont <strong>avec \u00e9tat<\/strong> : le serveur garde une trace de chaque connexion, ce qui rend la r\u00e9vocation imm\u00e9diate triviale. Les JWT sont sans \u00e9tat : ils passent \u00e0 l&#8217;\u00e9chelle sans base de donn\u00e9es de sessions, mais r\u00e9voquer un jeton avant son expiration demande un effort suppl\u00e9mentaire (liste de r\u00e9vocation, rotation des refresh tokens). Ce tutoriel adopte une approche hybride qui combine le meilleur des deux mondes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un point essentiel souvent mal compris : la charge utile d&#8217;un JWT n&#8217;est <strong>pas chiffr\u00e9e<\/strong>. Elle est seulement encod\u00e9e en Base64URL, donc lisible par quiconque intercepte le jeton. La signature emp\u00eache la modification, pas la lecture. Ne placez jamais de mot de passe, de num\u00e9ro de carte ou de donn\u00e9e sensible dans le payload. Si vous avez besoin de confidentialit\u00e9, il faut chiffrer le jeton (JWE), un sujet que nous abordons dans les astuces avanc\u00e9es.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequis-et-versions-logicielles-requises\">Pr\u00e9requis et versions logicielles requises<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de coder, v\u00e9rifiez votre environnement. Les versions ci-dessous sont celles test\u00e9es pour ce guide. Pour les composants o\u00f9 une version exacte n&#8217;est pas critique, la derni\u00e8re version stable convient parfaitement.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Composant<\/th><th>Version recommand\u00e9e<\/th><th>R\u00f4le dans le projet<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>22 LTS (ou derni\u00e8re LTS)<\/td><td>Environnement d&#8217;ex\u00e9cution serveur<\/td><\/tr><tr><td>npm<\/td><td>10 ou sup\u00e9rieur<\/td><td>Gestionnaire de paquets<\/td><\/tr><tr><td>jsonwebtoken<\/td><td>9.0.3<\/td><td>Signature et v\u00e9rification des JWT<\/td><\/tr><tr><td>express<\/td><td>derni\u00e8re version 4.x ou 5.x<\/td><td>Serveur HTTP et routage<\/td><\/tr><tr><td>bcrypt<\/td><td>derni\u00e8re version stable<\/td><td>Hachage des mots de passe<\/td><\/tr><tr><td>argon2<\/td><td>derni\u00e8re version stable<\/td><td>Alternative moderne \u00e0 bcrypt<\/td><\/tr><tr><td>dotenv<\/td><td>derni\u00e8re version stable<\/td><td>Gestion des variables d&#8217;environnement<\/td><\/tr><tr><td>cookie-parser<\/td><td>derni\u00e8re version stable<\/td><td>Lecture des cookies httpOnly<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">V\u00e9rifiez votre version de Node.js avec une seule commande. Si elle est inf\u00e9rieure \u00e0 18, mettez \u00e0 jour : les versions plus anciennes manquent du module <code>crypto.webcrypto<\/code> stable et de plusieurs correctifs de s\u00e9curit\u00e9. La liste officielle des versions LTS se trouve sur <a href=\"https:\/\/nodejs.org\/en\/about\/previous-releases\" target=\"_blank\" rel=\"noopener\">nodejs.org<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# Sortie attendue : v22.x.x (ou une version LTS plus r\u00e9cente)\n\nnpm --version\n# Sortie attendue : 10.x.x ou sup\u00e9rieur<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">C\u00f4t\u00e9 connaissances, vous devez \u00eatre \u00e0 l&#8217;aise avec JavaScript asynchrone (<code>async\/await<\/code>), les requ\u00eates HTTP et la notion de middleware Express. Une compr\u00e9hension de base des <a href=\"\/digital-signatures\/\">signatures num\u00e9riques<\/a> et des <a href=\"\/hash-functions\/\">fonctions de hachage cryptographiques<\/a> aide \u00e9norm\u00e9ment, car JWT repose enti\u00e8rement sur ces deux primitives.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"anatomie-dun-jwt-header-payload-et-signature\">Anatomie d&#8217;un JWT : header, payload et signature<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un JWT se compose de trois segments s\u00e9par\u00e9s par des points : <code>header.payload.signature<\/code>. Chaque segment est encod\u00e9 en Base64URL. Un jeton r\u00e9el ressemble \u00e0 ceci (raccourci pour la lisibilit\u00e9) :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3ODA1MDAwMDB9.dQw4w9WgXcQ_signature_tronquee<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"le-header-type-et-algorithme\">Le header : type et algorithme<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le premier segment d\u00e9crit le type du jeton et l&#8217;algorithme de signature. D\u00e9cod\u00e9, il donne un objet JSON minimal. Le champ <code>alg<\/code> est critique pour la s\u00e9curit\u00e9 : c&#8217;est pr\u00e9cis\u00e9ment ce champ que les attaquants tentent de manipuler avec l&#8217;attaque <code>alg: none<\/code>, dont nous parlerons plus loin.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"alg\": \"HS256\",\n  \"typ\": \"JWT\"\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"le-payload-les-revendications-claims\">Le payload : les revendications (claims)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le deuxi\u00e8me segment contient les claims. On distingue les claims enregistr\u00e9s, standardis\u00e9s par la RFC 7519, et les claims priv\u00e9s que vous d\u00e9finissez. Les claims enregistr\u00e9s les plus utiles sont <code>exp<\/code> (expiration), <code>iat<\/code> (date d&#8217;\u00e9mission), <code>sub<\/code> (sujet), <code>iss<\/code> (\u00e9metteur) et <code>aud<\/code> (audience). Point crucial : la biblioth\u00e8que <code>jsonwebtoken<\/code> n&#8217;applique <strong>aucune valeur par d\u00e9faut<\/strong> pour <code>expiresIn<\/code>, <code>audience<\/code>, <code>issuer<\/code> ou <code>subject<\/code>. Si vous ne les d\u00e9finissez pas, vos jetons n&#8217;expirent jamais.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"sub\": \"123\",\n  \"role\": \"admin\",\n  \"iat\": 1780496400,\n  \"exp\": 1780500000\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"la-signature-le-verrou-cryptographique\">La signature : le verrou cryptographique<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le troisi\u00e8me segment est calcul\u00e9 en signant <code>header.payload<\/code> avec une cl\u00e9 secr\u00e8te (HS256) ou une cl\u00e9 priv\u00e9e (RS256, ES256). Le serveur recalcule cette signature \u00e0 chaque requ\u00eate pour v\u00e9rifier que le jeton n&#8217;a pas \u00e9t\u00e9 modifi\u00e9. Changez un seul caract\u00e8re du payload et la signature ne correspond plus : la v\u00e9rification \u00e9choue. C&#8217;est exactement le principe des <a href=\"\/digital-signatures\/\">signatures num\u00e9riques<\/a> appliqu\u00e9 \u00e0 un format compact destin\u00e9 au web.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hs256-vs-rs256-vs-es256-choisir-le-bon-algorithme\">HS256 vs RS256 vs ES256 : choisir le bon algorithme<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le choix de l&#8217;algorithme conditionne l&#8217;architecture enti\u00e8re. HS256 est sym\u00e9trique : la m\u00eame cl\u00e9 signe et v\u00e9rifie. RS256 et ES256 sont asym\u00e9triques : une cl\u00e9 priv\u00e9e signe, une cl\u00e9 publique v\u00e9rifie. Ce d\u00e9tail change tout d\u00e8s que plusieurs services doivent valider le m\u00eame jeton.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Crit\u00e8re<\/th><th>HS256<\/th><th>RS256<\/th><th>ES256<\/th><\/tr><\/thead><tbody><tr><td>Type<\/td><td>Sym\u00e9trique (HMAC-SHA256)<\/td><td>Asym\u00e9trique (RSA)<\/td><td>Asym\u00e9trique (ECDSA P-256)<\/td><\/tr><tr><td>Cl\u00e9 de signature<\/td><td>Secret partag\u00e9<\/td><td>Cl\u00e9 priv\u00e9e RSA<\/td><td>Cl\u00e9 priv\u00e9e elliptique<\/td><\/tr><tr><td>Cl\u00e9 de v\u00e9rification<\/td><td>M\u00eame secret<\/td><td>Cl\u00e9 publique RSA<\/td><td>Cl\u00e9 publique elliptique<\/td><\/tr><tr><td>Taille de signature<\/td><td>Petite<\/td><td>Grande (256 octets pour RSA-2048)<\/td><td>Compacte (64 octets)<\/td><\/tr><tr><td>Cas d&#8217;usage id\u00e9al<\/td><td>Service unique, monolithe<\/td><td>Microservices, OIDC<\/td><td>Mobile, IoT, performances<\/td><\/tr><tr><td>Co\u00fbt de v\u00e9rification<\/td><td>Tr\u00e8s faible<\/td><td>\u00c9lev\u00e9<\/td><td>Mod\u00e9r\u00e9<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">R\u00e8gle pratique : si une seule application signe et v\u00e9rifie ses propres jetons, <strong>HS256 suffit<\/strong> et reste le plus simple. D\u00e8s que plusieurs services ind\u00e9pendants doivent v\u00e9rifier les jetons sans partager de secret, passez \u00e0 RS256 ou ES256. La cl\u00e9 publique se distribue librement, la cl\u00e9 priv\u00e9e reste sur le service d&#8217;authentification. ES256 produit des signatures plus compactes et des v\u00e9rifications plus rapides que RSA, ce qui en fait un excellent choix pour les contextes contraints. Pour approfondir la cryptographie \u00e0 courbes elliptiques, consultez notre guide sur les <a href=\"\/digital-signatures\/\">signatures num\u00e9riques ECDSA et EdDSA<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-1-initialiser-le-projet-node-js\">\u00c9tape 1 : initialiser le projet Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez un dossier de projet et initialisez-le avec npm. Nous utilisons les modules ES (type <code>module<\/code>) pour b\u00e9n\u00e9ficier de la syntaxe <code>import<\/code> moderne, d\u00e9sormais standard en 2026.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir jwt-auth-api &amp;&amp; cd jwt-auth-api\nnpm init -y\nnpm pkg set type=\"module\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Installez ensuite les d\u00e9pendances. Nous \u00e9pinglons <code>jsonwebtoken<\/code> \u00e0 la version 9.0.3, valid\u00e9e sans vuln\u00e9rabilit\u00e9 connue, et ajoutons Express, bcrypt, dotenv et cookie-parser.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install express jsonwebtoken@9.0.3 bcrypt dotenv cookie-parser\n# V\u00e9rifiez l'installation\nnpm list jsonwebtoken\n# jwt-auth-api@1.0.0\n# \u2514\u2500\u2500 jsonwebtoken@9.0.3<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-2-generer-et-stocker-un-secret-robuste\">\u00c9tape 2 : g\u00e9n\u00e9rer et stocker un secret robuste<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La s\u00e9curit\u00e9 de HS256 d\u00e9pend enti\u00e8rement de la force du secret. Un secret faible comme <code>\"secret\"<\/code> ou <code>\"123456\"<\/code> se casse par force brute en quelques secondes avec des outils comme hashcat. G\u00e9n\u00e9rez un secret al\u00e9atoire d&#8217;au moins 256 bits avec le module <code>crypto<\/code> int\u00e9gr\u00e9.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -e \"console.log(require('crypto').randomBytes(48).toString('hex'))\"\n# Exemple de sortie :\n# 9f2c8a1e4b7d3f6a0c5e8b2d1f4a7c9e3b6d0f8a2c5e7b1d4f9a3c6e8b0d2f5a7c1e4b6d8<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Copiez cette valeur dans un fichier <code>.env<\/code> que vous ajoutez imm\u00e9diatement \u00e0 <code>.gitignore<\/code>. Ne committez jamais un secret dans Git : c&#8217;est l&#8217;une des fuites les plus fr\u00e9quentes signal\u00e9es dans les <a href=\"\/data-breaches\/\">analyses de fuites de donn\u00e9es<\/a>. Pr\u00e9voyez deux secrets distincts, un pour les access tokens et un pour les refresh tokens.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Fichier .env\nACCESS_TOKEN_SECRET=9f2c8a1e4b7d3f6a0c5e8b2d1f4a7c9e3b6d0f8a2c5e7b1d4f9a3c6e8b0d2f5a\nREFRESH_TOKEN_SECRET=a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0fedcba9876543210\nPORT=3000<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-3-hacher-les-mots-de-passe-avec-bcrypt\">\u00c9tape 3 : hacher les mots de passe avec bcrypt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">JWT g\u00e8re la session, pas les mots de passe. Avant d&#8217;\u00e9mettre le moindre jeton, hachez le mot de passe de l&#8217;utilisateur. N&#8217;utilisez jamais SHA-256 seul ni un stockage en clair : seuls des algorithmes lents et r\u00e9sistants au mat\u00e9riel d\u00e9di\u00e9 conviennent. bcrypt reste un choix solide, avec un facteur de co\u00fbt (rounds) configurable. Pour une s\u00e9curit\u00e9 de pointe, argon2id est encore meilleur, comme d\u00e9taill\u00e9 dans notre guide d\u00e9di\u00e9 au <a href=\"\/argon2-password-hashing-nodejs\/\">hachage Argon2 en Node.js<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth\/password.js\nimport bcrypt from 'bcrypt';\n\nconst SALT_ROUNDS = 12;\n\nexport async function hashPassword(plain) {\n  return bcrypt.hash(plain, SALT_ROUNDS);\n}\n\nexport async function verifyPassword(plain, hash) {\n  return bcrypt.compare(plain, hash);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Un facteur de co\u00fbt de 12 offre un bon \u00e9quilibre en 2026 : assez lent pour d\u00e9courager les attaques par force brute, assez rapide pour ne pas bloquer le serveur sur chaque connexion. Mesurez le temps de hachage sur votre mat\u00e9riel et visez environ 250 ms par op\u00e9ration. Si vos serveurs sont plus puissants, augmentez \u00e0 13 ou 14.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-4-creer-la-fonction-de-generation-daccess-token\">\u00c9tape 4 : cr\u00e9er la fonction de g\u00e9n\u00e9ration d&#8217;access token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Place au c\u0153ur du sujet. La fonction <code>signAccessToken<\/code> encapsule l&#8217;appel \u00e0 <code>jwt.sign<\/code>. Notez les options explicites : <code>expiresIn<\/code> court (15 minutes), <code>issuer<\/code> et <code>audience<\/code> d\u00e9finis. Ces deux derniers claims permettront de rejeter un jeton \u00e9mis par un autre service.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth\/tokens.js\nimport jwt from 'jsonwebtoken';\nimport 'dotenv\/config';\n\nconst ACCESS_SECRET = process.env.ACCESS_TOKEN_SECRET;\nconst REFRESH_SECRET = process.env.REFRESH_TOKEN_SECRET;\n\nexport function signAccessToken(user) {\n  return jwt.sign(\n    { role: user.role },\n    ACCESS_SECRET,\n    {\n      subject: String(user.id),\n      expiresIn: '15m',\n      issuer: 'jwt-auth-api',\n      audience: 'jwt-auth-client',\n      algorithm: 'HS256',\n    }\n  );\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c9vitez d&#8217;entasser des donn\u00e9es dans le payload. Plus le jeton est gros, plus il alourdit chaque requ\u00eate HTTP. Mettez l&#8217;identifiant dans <code>sub<\/code>, le r\u00f4le si n\u00e9cessaire, et rien d&#8217;autre. Toute donn\u00e9e modifiable (nom, e-mail) doit \u00eatre recharg\u00e9e depuis la base \u00e0 partir de l&#8217;identifiant, sinon elle devient obsol\u00e8te d\u00e8s que l&#8217;utilisateur la change.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-5-generer-un-refresh-token-separe\">\u00c9tape 5 : g\u00e9n\u00e9rer un refresh token s\u00e9par\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;access token est volontairement \u00e9ph\u00e9m\u00e8re pour limiter les d\u00e9g\u00e2ts en cas de vol. Le refresh token, \u00e0 dur\u00e9e plus longue, permet d&#8217;obtenir un nouvel access token sans redemander le mot de passe. Il utilise un secret diff\u00e9rent et une dur\u00e9e de vie plus longue, par exemple 7 jours. La documentation de <code>jsonwebtoken<\/code> avertit que le rafra\u00eechissement automatique mal con\u00e7u introduit des failles : impl\u00e9mentez-le avec soin.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function signRefreshToken(user, tokenId) {\n  return jwt.sign(\n    { tokenId },\n    REFRESH_SECRET,\n    {\n      subject: String(user.id),\n      expiresIn: '7d',\n      issuer: 'jwt-auth-api',\n      audience: 'jwt-auth-client',\n      algorithm: 'HS256',\n    }\n  );\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le champ <code>tokenId<\/code> est la cl\u00e9 de la r\u00e9vocation. Stockez cet identifiant c\u00f4t\u00e9 serveur (base de donn\u00e9es ou Redis). Lors d&#8217;un rafra\u00eechissement, v\u00e9rifiez que le <code>tokenId<\/code> existe toujours et n&#8217;a pas \u00e9t\u00e9 invalid\u00e9. Cette approche r\u00e9introduit juste assez d&#8217;\u00e9tat pour permettre une d\u00e9connexion r\u00e9elle, sans sacrifier la scalabilit\u00e9 de l&#8217;access token.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Type de jeton<\/th><th>Dur\u00e9e recommand\u00e9e<\/th><th>Stockage c\u00f4t\u00e9 client<\/th><th>Secret utilis\u00e9<\/th><\/tr><\/thead><tbody><tr><td>Access token<\/td><td>5 \u00e0 15 minutes<\/td><td>M\u00e9moire JavaScript<\/td><td>ACCESS_TOKEN_SECRET<\/td><\/tr><tr><td>Refresh token<\/td><td>7 \u00e0 30 jours<\/td><td>Cookie httpOnly + Secure<\/td><td>REFRESH_TOKEN_SECRET<\/td><\/tr><tr><td>Jeton de r\u00e9initialisation<\/td><td>15 \u00e0 60 minutes<\/td><td>Jamais persist\u00e9 c\u00f4t\u00e9 client<\/td><td>Secret d\u00e9di\u00e9<\/td><\/tr><tr><td>Jeton de v\u00e9rification e-mail<\/td><td>24 heures<\/td><td>Lien \u00e0 usage unique<\/td><td>Secret d\u00e9di\u00e9<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-6-construire-le-serveur-express-et-la-route-de-connexion\">\u00c9tape 6 : construire le serveur Express et la route de connexion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Assemblons maintenant le serveur. Pour ce tutoriel, nous simulons une base de donn\u00e9es avec un tableau en m\u00e9moire. En production, remplacez-le par PostgreSQL, MongoDB ou tout autre stockage. La route <code>\/login<\/code> v\u00e9rifie le mot de passe hach\u00e9, puis \u00e9met les deux jetons.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js\nimport express from 'express';\nimport cookieParser from 'cookie-parser';\nimport crypto from 'crypto';\nimport 'dotenv\/config';\nimport { hashPassword, verifyPassword } from '.\/auth\/password.js';\nimport { signAccessToken, signRefreshToken } from '.\/auth\/tokens.js';\n\nconst app = express();\napp.use(express.json());\napp.use(cookieParser());\n\n\/\/ Base de donnees simulee\nconst users = [];\nconst validRefreshTokens = new Set();\n\n\/\/ Inscription\napp.post('\/register', async (req, res) =&gt; {\n  const { email, password } = req.body;\n  if (!email || !password) {\n    return res.status(400).json({ error: 'Champs manquants' });\n  }\n  if (users.find(u =&gt; u.email === email)) {\n    return res.status(409).json({ error: 'Utilisateur existant' });\n  }\n  const hash = await hashPassword(password);\n  const user = { id: users.length + 1, email, passwordHash: hash, role: 'user' };\n  users.push(user);\n  res.status(201).json({ id: user.id, email: user.email });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La route de connexion suit la m\u00eame logique mais \u00e9met les jetons. Le refresh token part dans un cookie <code>httpOnly<\/code>, inaccessible au JavaScript du navigateur, ce qui le prot\u00e8ge du vol par XSS. L&#8217;access token est renvoy\u00e9 dans le corps de la r\u00e9ponse, \u00e0 conserver en m\u00e9moire c\u00f4t\u00e9 client.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Connexion\napp.post('\/login', async (req, res) =&gt; {\n  const { email, password } = req.body;\n  const user = users.find(u =&gt; u.email === email);\n  if (!user) {\n    return res.status(401).json({ error: 'Identifiants invalides' });\n  }\n  const ok = await verifyPassword(password, user.passwordHash);\n  if (!ok) {\n    return res.status(401).json({ error: 'Identifiants invalides' });\n  }\n\n  const tokenId = crypto.randomUUID();\n  validRefreshTokens.add(tokenId);\n\n  const accessToken = signAccessToken(user);\n  const refreshToken = signRefreshToken(user, tokenId);\n\n  res.cookie('refreshToken', refreshToken, {\n    httpOnly: true,\n    secure: true,\n    sameSite: 'strict',\n    maxAge: 7 * 24 * 60 * 60 * 1000,\n  });\n\n  res.json({ accessToken });\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-7-ecrire-le-middleware-de-verification\">\u00c9tape 7 : \u00e9crire le middleware de v\u00e9rification<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le middleware prot\u00e8ge les routes priv\u00e9es. Il extrait le jeton de l&#8217;en-t\u00eate <code>Authorization: Bearer<\/code>, le v\u00e9rifie, et attache l&#8217;utilisateur d\u00e9cod\u00e9 \u00e0 la requ\u00eate. Point capital de s\u00e9curit\u00e9 : passez explicitement l&#8217;option <code>algorithms: ['HS256']<\/code>. Sans cette liste blanche, un attaquant peut tenter de basculer l&#8217;algorithme et contourner la v\u00e9rification.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth\/middleware.js\nimport jwt from 'jsonwebtoken';\nimport 'dotenv\/config';\n\nexport function authenticate(req, res, next) {\n  const header = req.headers['authorization'];\n  if (!header || !header.startsWith('Bearer ')) {\n    return res.status(401).json({ error: 'Jeton absent' });\n  }\n  const token = header.split(' ')[1];\n\n  try {\n    const payload = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, {\n      algorithms: ['HS256'],\n      issuer: 'jwt-auth-api',\n      audience: 'jwt-auth-client',\n    });\n    req.user = { id: payload.sub, role: payload.role };\n    next();\n  } catch (err) {\n    if (err.name === 'TokenExpiredError') {\n      return res.status(401).json({ error: 'Jeton expire' });\n    }\n    return res.status(403).json({ error: 'Jeton invalide' });\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Distinguez bien les codes d&#8217;erreur. Un <code>TokenExpiredError<\/code> renvoie 401 pour signaler au client qu&#8217;il doit rafra\u00eechir son jeton. Une signature invalide renvoie 403 : le jeton est falsifi\u00e9, il n&#8217;y a rien \u00e0 rafra\u00eechir. Cette distinction permet au client de r\u00e9agir intelligemment plut\u00f4t que de d\u00e9connecter l&#8217;utilisateur au moindre incident.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-8-proteger-une-route-et-tester-le-flux\">\u00c9tape 8 : prot\u00e9ger une route et tester le flux<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ajoutez une route prot\u00e9g\u00e9e qui renvoie le profil de l&#8217;utilisateur connect\u00e9. Le middleware <code>authenticate<\/code> s&#8217;ex\u00e9cute avant le gestionnaire.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { authenticate } from '.\/auth\/middleware.js';\n\napp.get('\/me', authenticate, (req, res) =&gt; {\n  const user = users.find(u =&gt; u.id === Number(req.user.id));\n  if (!user) return res.status(404).json({ error: 'Introuvable' });\n  res.json({ id: user.id, email: user.email, role: user.role });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () =&gt; console.log(`API sur le port ${PORT}`));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Testez le flux complet avec curl. Inscrivez un utilisateur, connectez-vous, r\u00e9cup\u00e9rez l&#8217;access token, puis appelez la route prot\u00e9g\u00e9e.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># 1. Inscription\ncurl -X POST http:\/\/localhost:3000\/register \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"email\":\"alice@example.com\",\"password\":\"MotDePasseFort!2026\"}'\n\n# 2. Connexion (recupere l'access token)\ncurl -X POST http:\/\/localhost:3000\/login \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"email\":\"alice@example.com\",\"password\":\"MotDePasseFort!2026\"}'\n# Sortie : {\"accessToken\":\"eyJhbGciOiJIUzI1NiI...\"}\n\n# 3. Route protegee\ncurl http:\/\/localhost:3000\/me \\\n  -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiI...\"\n# Sortie : {\"id\":1,\"email\":\"alice@example.com\",\"role\":\"user\"}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-9-implementer-la-rotation-des-refresh-tokens\">\u00c9tape 9 : impl\u00e9menter la rotation des refresh tokens<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La rotation est la meilleure d\u00e9fense contre le vol de refresh token. \u00c0 chaque rafra\u00eechissement, on invalide l&#8217;ancien <code>tokenId<\/code> et on en \u00e9met un nouveau. Si un jeton vol\u00e9 est r\u00e9utilis\u00e9 apr\u00e8s rotation, le serveur d\u00e9tecte la r\u00e9utilisation et r\u00e9voque toute la cha\u00eene, for\u00e7ant une reconnexion. Pour la th\u00e9orie, consultez l&#8217;analyse d&#8217;<a href=\"https:\/\/auth0.com\/blog\/refresh-tokens-what-are-they-and-when-to-use-them\/\" target=\"_blank\" rel=\"noopener\">Auth0 sur les refresh tokens<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app.post('\/refresh', (req, res) =&gt; {\n  const token = req.cookies.refreshToken;\n  if (!token) return res.status(401).json({ error: 'Pas de refresh token' });\n\n  try {\n    const payload = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET, {\n      algorithms: ['HS256'],\n      issuer: 'jwt-auth-api',\n      audience: 'jwt-auth-client',\n    });\n\n    \/\/ Detection de reutilisation\n    if (!validRefreshTokens.has(payload.tokenId)) {\n      return res.status(403).json({ error: 'Refresh token revoque' });\n    }\n\n    \/\/ Rotation : on invalide l'ancien, on emet un nouveau\n    validRefreshTokens.delete(payload.tokenId);\n    const newTokenId = crypto.randomUUID();\n    validRefreshTokens.add(newTokenId);\n\n    const user = users.find(u =&gt; u.id === Number(payload.sub));\n    const accessToken = signAccessToken(user);\n    const refreshToken = signRefreshToken(user, newTokenId);\n\n    res.cookie('refreshToken', refreshToken, {\n      httpOnly: true, secure: true, sameSite: 'strict',\n      maxAge: 7 * 24 * 60 * 60 * 1000,\n    });\n    res.json({ accessToken });\n  } catch (err) {\n    return res.status(403).json({ error: 'Refresh token invalide' });\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-10-gerer-la-deconnexion-et-la-revocation\">\u00c9tape 10 : g\u00e9rer la d\u00e9connexion et la r\u00e9vocation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Comme les JWT sont sans \u00e9tat, un access token reste valide jusqu&#8217;\u00e0 son expiration m\u00eame apr\u00e8s d\u00e9connexion. C&#8217;est pourquoi on garde l&#8217;access token court (15 minutes maximum). La d\u00e9connexion consiste \u00e0 supprimer le refresh token c\u00f4t\u00e9 serveur et \u00e0 effacer le cookie c\u00f4t\u00e9 client. L&#8217;access token restant expirera de lui-m\u00eame en quelques minutes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app.post('\/logout', (req, res) =&gt; {\n  const token = req.cookies.refreshToken;\n  if (token) {\n    try {\n      const payload = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET, {\n        algorithms: ['HS256'],\n      });\n      validRefreshTokens.delete(payload.tokenId);\n    } catch (_) { \/* jeton deja invalide, on ignore *\/ }\n  }\n  res.clearCookie('refreshToken');\n  res.json({ message: 'Deconnexion reussie' });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pour une r\u00e9vocation imm\u00e9diate des access tokens (cas d&#8217;un compte compromis), maintenez une liste de r\u00e9vocation (denylist) en Redis, index\u00e9e par <code>sub<\/code> ou par identifiant de jeton, avec une expiration automatique cal\u00e9e sur la dur\u00e9e de l&#8217;access token. Le middleware consulte cette liste \u00e0 chaque requ\u00eate. C&#8217;est un compromis : on perd un peu de l&#8217;avantage sans \u00e9tat, mais on gagne une r\u00e9vocation instantan\u00e9e.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-11-localstorage-ou-cookie-httponly-le-vrai-debat\">\u00c9tape 11 : localStorage ou cookie httpOnly, le vrai d\u00e9bat<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le stockage c\u00f4t\u00e9 client divise la communaut\u00e9 depuis des ann\u00e9es. Voici la position d\u00e9fendable en 2026, align\u00e9e sur les bonnes pratiques de l&#8217;<a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/JSON_Web_Token_for_Java_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP JWT Cheat Sheet<\/a>.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>localStorage<\/strong> : facile \u00e0 utiliser, mais expos\u00e9 au XSS. Tout script inject\u00e9 dans la page peut lire le jeton. \u00c0 \u00e9viter pour les jetons sensibles.<\/li><li><strong>Cookie httpOnly + Secure + SameSite<\/strong> : invisible au JavaScript, donc prot\u00e9g\u00e9 du XSS, mais expos\u00e9 au CSRF si mal configur\u00e9. L&#8217;attribut <code>SameSite=strict<\/code> neutralise la plupart des attaques CSRF.<\/li><li><strong>M\u00e9moire JavaScript (variable)<\/strong> : l&#8217;access token vit dans une variable, jamais persist\u00e9. Dispara\u00eet au rafra\u00eechissement de la page, mais c&#8217;est l&#8217;option la plus s\u00fbre contre le vol persistant.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;architecture recommand\u00e9e : access token en m\u00e9moire, refresh token en cookie httpOnly. Au chargement de la page, le client appelle <code>\/refresh<\/code> pour obtenir un nouvel access token \u00e0 partir du cookie. Cette combinaison r\u00e9siste \u00e0 la fois au XSS (le refresh token est inaccessible au script) et au vol persistant (l&#8217;access token dispara\u00eet \u00e0 la fermeture de l&#8217;onglet). Servez imp\u00e9rativement le tout en HTTPS, comme expliqu\u00e9 dans notre article sur <a href=\"\/https-tls-explained\/\">HTTPS et TLS<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-12-le-projet-complet-assemble\">\u00c9tape 12 : le projet complet assembl\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voici la structure finale du projet. Chaque responsabilit\u00e9 est isol\u00e9e dans son module, ce qui facilite les tests et la maintenance.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>jwt-auth-api\/\n\u251c\u2500\u2500 .env                 # secrets (jamais commite)\n\u251c\u2500\u2500 .gitignore           # ignore .env et node_modules\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 server.js            # serveur Express et routes\n\u2514\u2500\u2500 auth\/\n    \u251c\u2500\u2500 password.js      # hachage bcrypt\n    \u251c\u2500\u2500 tokens.js        # signature des JWT\n    \u2514\u2500\u2500 middleware.js    # verification des JWT<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lancez le serveur avec <code>node server.js<\/code>. Le flux complet fonctionne : inscription, connexion, acc\u00e8s prot\u00e9g\u00e9, rafra\u00eechissement, d\u00e9connexion. Pour passer en production, remplacez le tableau <code>users<\/code> par une vraie base, d\u00e9placez <code>validRefreshTokens<\/code> vers Redis, ajoutez une limitation de d\u00e9bit (rate limiting) sur <code>\/login<\/code> et activez les en-t\u00eates de s\u00e9curit\u00e9 avec helmet. Ce squelette respecte d\u00e9j\u00e0 l&#8217;essentiel des recommandations de la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8725\" target=\"_blank\" rel=\"noopener\">RFC 8725<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cinq-pieges-courants-qui-font-tomber-votre-securite-jwt\">Cinq pi\u00e8ges courants qui font tomber votre s\u00e9curit\u00e9 JWT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La plupart des failles JWT ne viennent pas de la cryptographie, mais d&#8217;erreurs d&#8217;impl\u00e9mentation. Voici les cinq plus fr\u00e9quentes et comment les \u00e9viter.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-1-ne-pas-verrouiller-lalgorithme\">Pi\u00e8ge 1 : ne pas verrouiller l&#8217;algorithme<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;attaque <code>alg: none<\/code> exploite les impl\u00e9mentations qui font confiance au champ <code>alg<\/code> du jeton. L&#8217;attaquant le passe \u00e0 <code>none<\/code>, supprime la signature, et certaines biblioth\u00e8ques acceptent le jeton sans v\u00e9rification. La parade : toujours passer <code>algorithms: ['HS256']<\/code> \u00e0 <code>jwt.verify<\/code>. C&#8217;est non n\u00e9gociable. Une variante consiste \u00e0 transformer un RS256 en HS256 pour signer avec la cl\u00e9 publique connue : la liste blanche bloque aussi cette attaque.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-2-utiliser-un-secret-faible-ou-reutilise\">Pi\u00e8ge 2 : utiliser un secret faible ou r\u00e9utilis\u00e9<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Un secret court ou pr\u00e9visible se casse par force brute. Utilisez au minimum 256 bits d&#8217;entropie al\u00e9atoire, comme \u00e0 l&#8217;\u00e9tape 2. N&#8217;utilisez jamais le m\u00eame secret pour les access et refresh tokens, ni le m\u00eame secret entre environnements de d\u00e9veloppement et de production.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-3-oublier-lexpiration\">Pi\u00e8ge 3 : oublier l&#8217;expiration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rappel critique : <code>jsonwebtoken<\/code> n&#8217;impose aucune expiration par d\u00e9faut. Un jeton sans <code>expiresIn<\/code> reste valable pour toujours. S&#8217;il est vol\u00e9, l&#8217;attaquant a un acc\u00e8s permanent. D\u00e9finissez toujours <code>expiresIn<\/code> explicitement, court pour les access tokens.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-4-stocker-des-donnees-sensibles-dans-le-payload\">Pi\u00e8ge 4 : stocker des donn\u00e9es sensibles dans le payload<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le payload est encod\u00e9, pas chiffr\u00e9. N&#8217;importe qui peut le d\u00e9coder sur <a href=\"https:\/\/jwt.io\/\" target=\"_blank\" rel=\"noopener\">jwt.io<\/a>. Mots de passe, cl\u00e9s API, donn\u00e9es personnelles n&#8217;ont rien \u00e0 y faire. Si vous devez transporter des donn\u00e9es confidentielles dans un jeton, utilisez JWE (JSON Web Encryption).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-5-conserver-laccess-token-dans-localstorage\">Pi\u00e8ge 5 : conserver l&#8217;access token dans localStorage<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Comme vu \u00e0 l&#8217;\u00e9tape 11, localStorage est lisible par tout script. Une seule faille XSS et tous vos jetons fuient. Pr\u00e9f\u00e9rez la m\u00e9moire pour l&#8217;access token et un cookie httpOnly pour le refresh token.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vulnerabilites-jwt-et-durcissement-avance\">Vuln\u00e9rabilit\u00e9s JWT et durcissement avanc\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Au-del\u00e0 des cinq pi\u00e8ges, plusieurs vecteurs m\u00e9ritent attention. L&#8217;historique de <code>jsonwebtoken<\/code> recense des correctifs comme la CVE-2022-23529, d\u00e9sormais corrig\u00e9e dans les versions r\u00e9centes. Rester sur la 9.0.3, sans vuln\u00e9rabilit\u00e9 connue selon Snyk, est la premi\u00e8re ligne de d\u00e9fense. La s\u00e9curit\u00e9 de votre cha\u00eene d\u00e9pend aussi de vos d\u00e9pendances : auditez-les r\u00e9guli\u00e8rement avec <code>npm audit<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La confusion d&#8217;algorithme (algorithm confusion) reste le pi\u00e8ge le plus subtil avec les cl\u00e9s asym\u00e9triques. Si votre service utilise RS256 et qu&#8217;un attaquant force HS256 en signant avec votre cl\u00e9 publique RSA (qui est, par d\u00e9finition, publique), une v\u00e9rification na\u00efve l&#8217;accepte. Encore une fois, la liste blanche d&#8217;algorithmes c\u00f4t\u00e9 <code>verify<\/code> est la parade. Validez aussi syst\u00e9matiquement <code>iss<\/code> et <code>aud<\/code> pour rejeter les jetons d&#8217;un autre \u00e9metteur ou destin\u00e9s \u00e0 une autre audience.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pensez \u00e0 la limitation de d\u00e9bit sur les routes d&#8217;authentification. Sans elle, <code>\/login<\/code> devient une cible de bourrage d&#8217;identifiants (credential stuffing). Couplez JWT avec une <a href=\"\/password-security\/\">politique de mots de passe robuste<\/a> et id\u00e9alement une authentification \u00e0 deux facteurs. JWT authentifie une session, il ne remplace pas une strat\u00e9gie d&#8217;identit\u00e9 compl\u00e8te. Le guide <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener\">OWASP Top 10<\/a> recense ces vecteurs dans la cat\u00e9gorie des d\u00e9faillances d&#8217;authentification.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Vuln\u00e9rabilit\u00e9<\/th><th>M\u00e9canisme<\/th><th>Contre-mesure<\/th><\/tr><\/thead><tbody><tr><td>alg: none<\/td><td>Suppression de la signature<\/td><td>Liste blanche d&#8217;algorithmes au verify<\/td><\/tr><tr><td>Confusion d&#8217;algorithme<\/td><td>RS256 forc\u00e9 en HS256<\/td><td>Algorithme impos\u00e9 explicitement<\/td><\/tr><tr><td>Secret faible<\/td><td>Force brute du secret HMAC<\/td><td>Secret de 256 bits al\u00e9atoire<\/td><\/tr><tr><td>Absence d&#8217;expiration<\/td><td>Jeton valable ind\u00e9finiment<\/td><td>expiresIn court obligatoire<\/td><\/tr><tr><td>Vol par XSS<\/td><td>Lecture du jeton en localStorage<\/td><td>Cookie httpOnly + access en m\u00e9moire<\/td><\/tr><tr><td>Rejeu de refresh token<\/td><td>R\u00e9utilisation d&#8217;un jeton vol\u00e9<\/td><td>Rotation et d\u00e9tection de r\u00e9utilisation<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"depannage-huit-problemes-jwt-frequents-et-leurs-solutions\">D\u00e9pannage : huit probl\u00e8mes JWT fr\u00e9quents et leurs solutions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voici les erreurs que vous rencontrerez le plus souvent en d\u00e9veloppant, avec leur cause et leur r\u00e9solution.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>JsonWebTokenError: invalid signature<\/strong> : le secret de v\u00e9rification ne correspond pas \u00e0 celui de signature. V\u00e9rifiez que <code>.env<\/code> est bien charg\u00e9 et que vous n&#8217;utilisez pas le secret du refresh token pour v\u00e9rifier un access token.<\/li><li><strong>TokenExpiredError: jwt expired<\/strong> : comportement normal, l&#8217;access token a d\u00e9pass\u00e9 sa dur\u00e9e de vie. Le client doit appeler <code>\/refresh<\/code>. Ne traitez pas cela comme une erreur fatale.<\/li><li><strong>JsonWebTokenError: jwt malformed<\/strong> : le jeton transmis n&#8217;a pas le format <code>a.b.c<\/code>. V\u00e9rifiez que vous extrayez bien la partie apr\u00e8s <code>Bearer <\/code> sans inclure le mot-cl\u00e9.<\/li><li><strong>JsonWebTokenError: jwt audience invalid<\/strong> : le claim <code>aud<\/code> du jeton ne correspond pas \u00e0 l&#8217;audience attendue au verify. Alignez les valeurs entre signature et v\u00e9rification.<\/li><li><strong>secretOrPrivateKey must have a value<\/strong> : la variable d&#8217;environnement est <code>undefined<\/code>. Confirmez que <code>import 'dotenv\/config'<\/code> s&#8217;ex\u00e9cute avant la lecture du secret.<\/li><li><strong>Le cookie refreshToken n&#8217;est pas envoy\u00e9<\/strong> : en HTTPS local manquant, l&#8217;attribut <code>secure: true<\/code> bloque le cookie. En d\u00e9veloppement, testez derri\u00e8re un proxy HTTPS ou un certificat local.<\/li><li><strong>CORS bloque la requ\u00eate avec credentials<\/strong> : ajoutez <code>credentials: true<\/code> c\u00f4t\u00e9 serveur et <code>withCredentials<\/code> c\u00f4t\u00e9 client, et sp\u00e9cifiez une origine exacte (pas le joker).<\/li><li><strong>req.user est undefined<\/strong> : le middleware <code>authenticate<\/code> n&#8217;est pas appliqu\u00e9 \u00e0 la route, ou il a \u00e9chou\u00e9 silencieusement. V\u00e9rifiez l&#8217;ordre des middlewares et la pr\u00e9sence de l&#8217;en-t\u00eate Authorization.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"astuces-avancees-jose-jwks-et-cles-asymetriques\">Astuces avanc\u00e9es : jose, JWKS et cl\u00e9s asym\u00e9triques<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pour les architectures distribu\u00e9es modernes, la biblioth\u00e8que <a href=\"https:\/\/github.com\/panva\/jose\" target=\"_blank\" rel=\"noopener\">jose<\/a> est une alternative puissante \u00e0 <code>jsonwebtoken<\/code>. Elle impl\u00e9mente l&#8217;ensemble des standards JOSE (JWT, JWS, JWE, JWK) en s&#8217;appuyant sur les API Web Crypto natives, avec un support TypeScript et ESM de premier ordre. Elle brille particuli\u00e8rement avec les cl\u00e9s asym\u00e9triques et la r\u00e9cup\u00e9ration de cl\u00e9s via un endpoint JWKS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Verification avec jose et un JWKS distant (RS256)\nimport * as jose from 'jose';\n\nconst JWKS = jose.createRemoteJWKSet(\n  new URL('https:\/\/votre-fournisseur\/.well-known\/jwks.json')\n);\n\nconst { payload } = await jose.jwtVerify(token, JWKS, {\n  issuer: 'https:\/\/votre-fournisseur\/',\n  audience: 'votre-api',\n});\nconsole.log(payload.sub);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cette approche est la norme pour OpenID Connect et les fournisseurs d&#8217;identit\u00e9 comme Auth0, Keycloak ou les services cloud. Votre API n&#8217;a jamais besoin du secret : elle t\u00e9l\u00e9charge les cl\u00e9s publiques depuis l&#8217;endpoint JWKS, les met en cache, et v\u00e9rifie les jetons en local. La rotation des cl\u00e9s c\u00f4t\u00e9 fournisseur est transparente. Pour comprendre les fondations cryptographiques sous-jacentes, notre article sur <a href=\"\/sha-256\/\">SHA-256<\/a> compl\u00e8te utilement ce tutoriel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Derni\u00e8re astuce de production : journalisez les \u00e9checs de v\u00e9rification sans jamais journaliser le jeton complet. Un jeton dans les logs \u00e9quivaut \u00e0 un mot de passe en clair. Enregistrez plut\u00f4t le <code>sub<\/code>, la raison de l&#8217;\u00e9chec et l&#8217;horodatage. Couplez cela \u00e0 une surveillance des pics d&#8217;erreurs 401\/403 pour d\u00e9tecter une attaque en cours. Le code source officiel de <a href=\"https:\/\/github.com\/auth0\/node-jsonwebtoken\" target=\"_blank\" rel=\"noopener\">node-jsonwebtoken<\/a> reste la meilleure r\u00e9f\u00e9rence pour les options disponibles.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"foire-aux-questions-sur-lauthentification-jwt\">Foire aux questions sur l&#8217;authentification JWT<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"un-jwt-est-il-chiffre\">Un JWT est-il chiffr\u00e9 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Non. Un JWT standard (JWS) est sign\u00e9, pas chiffr\u00e9. Son payload est encod\u00e9 en Base64URL, donc lisible par quiconque l&#8217;intercepte. La signature garantit l&#8217;int\u00e9grit\u00e9, pas la confidentialit\u00e9. Pour chiffrer le contenu, il faut utiliser JWE.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quelle-duree-de-vie-choisir-pour-un-access-token\">Quelle dur\u00e9e de vie choisir pour un access token ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Entre 5 et 15 minutes pour la plupart des applications. Plus c&#8217;est court, moins un jeton vol\u00e9 est dangereux. Le refresh token, plus long (7 \u00e0 30 jours), \u00e9vite \u00e0 l&#8217;utilisateur de se reconnecter sans cesse. La valeur exacte d\u00e9pend de votre mod\u00e8le de menace.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"jwt-ou-sessions-classiques-lequel-choisir\">JWT ou sessions classiques, lequel choisir ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Les sessions conviennent aux applications monolithiques o\u00f9 la r\u00e9vocation imm\u00e9diate compte. JWT excelle dans les architectures distribu\u00e9es et les API consomm\u00e9es par plusieurs clients. L&#8217;approche hybride de ce tutoriel, access token JWT plus refresh token suivi en base, combine les avantages des deux.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"comment-revoquer-un-jwt-avant-son-expiration\">Comment r\u00e9voquer un JWT avant son expiration ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Par nature, un access token sans \u00e9tat ne se r\u00e9voque pas avant expiration. Les solutions : garder l&#8217;access token tr\u00e8s court, maintenir une denylist en Redis, et suivre les refresh tokens c\u00f4t\u00e9 serveur pour pouvoir couper le renouvellement. La rotation d\u00e9tecte aussi les jetons vol\u00e9s r\u00e9utilis\u00e9s.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"jsonwebtoken-ou-jose-quelle-bibliotheque-utiliser\">jsonwebtoken ou jose, quelle biblioth\u00e8que utiliser ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>jsonwebtoken<\/code> (9.0.3) reste id\u00e9al pour HS256 et les projets simples : API stable et documentation abondante. <code>jose<\/code> s&#8217;impose pour les standards complets (JWE, JWK, JWKS), les cl\u00e9s asym\u00e9triques et les contextes OpenID Connect. Les deux sont maintenus en 2026.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"dois-je-hacher-le-mot-de-passe-si-jutilise-jwt\">Dois-je hacher le mot de passe si j&#8217;utilise JWT ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Absolument. JWT g\u00e8re la session apr\u00e8s connexion, il ne remplace pas le stockage s\u00e9curis\u00e9 des mots de passe. Hachez toujours avec bcrypt ou, mieux, argon2id avant de comparer. JWT et hachage de mot de passe r\u00e9solvent deux probl\u00e8mes diff\u00e9rents et compl\u00e9mentaires.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hs256-est-il-moins-sur-que-rs256\">HS256 est-il moins s\u00fbr que RS256 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Non, \u00e0 force de cl\u00e9 \u00e9gale. La diff\u00e9rence est architecturale : HS256 partage un secret, RS256 s\u00e9pare signature et v\u00e9rification. Pour un service unique, HS256 avec un secret de 256 bits est parfaitement s\u00fbr. RS256 devient n\u00e9cessaire quand plusieurs services doivent v\u00e9rifier sans partager le secret.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conclusion-une-base-jwt-prete-pour-la-production\">Conclusion : une base JWT pr\u00eate pour la production<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Vous disposez d\u00e9sormais d&#8217;un syst\u00e8me d&#8217;authentification JWT complet en Node.js : hachage bcrypt des mots de passe, access tokens courts, refresh tokens avec rotation, middleware verrouill\u00e9 sur HS256, et stockage client r\u00e9sistant au XSS. Les douze \u00e9tapes couvrent l&#8217;essentiel des recommandations de la RFC 8725 et de l&#8217;OWASP. Le reste rel\u00e8ve de l&#8217;industrialisation : base de donn\u00e9es r\u00e9elle, Redis pour les jetons, rate limiting et surveillance.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retenez les trois r\u00e8gles qui \u00e9vitent 90 % des failles : verrouillez l&#8217;algorithme au verify, imposez une expiration courte, ne stockez jamais l&#8217;access token dans localStorage. Avec ces principes et le code de ce tutoriel, votre API repose sur des fondations solides pour 2026.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pour-aller-plus-loin-related-coverage\">Pour aller plus loin (Related Coverage)<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/argon2-password-hashing-nodejs\/\">Hachage de mot de passe Argon2 en Node.js : 11 \u00e9tapes<\/a><\/li><li><a href=\"\/aes-256-encryption-nodejs\/\">Chiffrement AES-256 en Node.js : 12 \u00e9tapes<\/a><\/li><li><a href=\"\/digital-signatures\/\">Les signatures num\u00e9riques expliqu\u00e9es<\/a><\/li><li><a href=\"\/hash-functions\/\">Les fonctions de hachage cryptographiques expliqu\u00e9es<\/a><\/li><li><a href=\"\/https-tls-explained\/\">HTTPS et TLS : ce que le cadenas signifie vraiment<\/a><\/li><li><a href=\"\/sha-256\/\">SHA-256 expliqu\u00e9 : fonctionnement et importance<\/a><\/li><li><a href=\"\/security\/\">S\u00e9curit\u00e9 en ligne : guide pratique (page pilier)<\/a><\/li><\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Le jeton JWT (JSON Web Token) est devenu le standard de fait pour l&#8217;authentification sans \u00e9tat dans les API modernes. En 2026, la biblioth\u00e8que jsonwebtoken reste la r\u00e9f\u00e9rence c\u00f4t\u00e9 Node.js\u2026<\/p>\n","protected":false},"author":5,"featured_media":62,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-61","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/61","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\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/comments?post=61"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/61\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media\/62"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media?parent=61"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/categories?post=61"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/tags?post=61"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}