{"id":117,"date":"2026-06-13T16:24:23","date_gmt":"2026-06-13T16:24:23","guid":{"rendered":"https:\/\/shattered.io\/fr\/2026\/06\/13\/oauth2-openid-connect-nodejs\/"},"modified":"2026-06-13T16:25:45","modified_gmt":"2026-06-13T16:25:45","slug":"oauth2-openid-connect-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/fr\/2026\/06\/13\/oauth2-openid-connect-nodejs\/","title":{"rendered":"OAuth2 en Node.js : flux s\u00e9curis\u00e9 en 12 \u00e9tapes [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Un identifiant vol\u00e9 reste la porte d&#8217;entr\u00e9e num\u00e9ro un des attaquants. En 2026, d\u00e9l\u00e9guer l&#8217;authentification \u00e0 un fournisseur d&#8217;identit\u00e9 via <strong>OAuth2<\/strong> et <strong>OpenID Connect<\/strong> n&#8217;est plus une option avanc\u00e9e, c&#8217;est la base d&#8217;une application web s\u00e9rieuse. Ce tutoriel vous guide pas \u00e0 pas pour construire, en Node.js, un flux d&#8217;authentification complet, conforme \u00e0 OAuth 2.1, avec PKCE, v\u00e9rification d&#8217;<strong>ID token<\/strong>, sessions chiffr\u00e9es et rotation des <strong>refresh tokens<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Vous repartirez avec un projet fonctionnel : un serveur Express qui redirige l&#8217;utilisateur vers un fournisseur OIDC, \u00e9change le code d&#8217;autorisation contre des jetons, s\u00e9curise la session par cookie et d\u00e9tecte le vol de refresh token. Comptez environ 45 minutes pour suivre les 12 \u00e9tapes. Le code utilise <code>openid-client<\/code> 6.8.4, la biblioth\u00e8que de r\u00e9f\u00e9rence maintenue par Filip Skokan, et <code>jose<\/code> 6.2.3 pour la cryptographie des jetons.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"oauth2-et-openid-connect-comprendre-la-difference-avant-de-coder\">OAuth2 et OpenID Connect : comprendre la diff\u00e9rence avant de coder<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La confusion entre <strong>OAuth2<\/strong> et <strong>OpenID Connect<\/strong> provoque la majorit\u00e9 des failles d&#8217;authentification. Les deux protocoles cohabitent dans le m\u00eame flux, mais ils r\u00e9pondent \u00e0 deux questions distinctes. OAuth2 (d\u00e9fini par la RFC 6749) est un cadre d&#8217;<em>autorisation<\/em>. Il r\u00e9pond \u00e0 : \u00ab cette application a-t-elle le droit d&#8217;acc\u00e9der \u00e0 cette ressource ? \u00bb. Son livrable est l&#8217;<strong>access token<\/strong>, un jeton que le client pr\u00e9sente \u00e0 une API pour obtenir des donn\u00e9es.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OpenID Connect (OIDC) est une couche d&#8217;<em>identit\u00e9<\/em> pos\u00e9e sur OAuth2. Il r\u00e9pond \u00e0 : \u00ab qui est cet utilisateur ? \u00bb. Son livrable est l&#8217;<strong>ID token<\/strong>, un JWT sign\u00e9 qui prouve l&#8217;identit\u00e9 du visiteur et contient des \u00ab claims \u00bb (sujet, email, nom, date d&#8217;\u00e9mission). Utiliser un access token pour authentifier un utilisateur est une erreur classique : l&#8217;access token est destin\u00e9 \u00e0 une API, pas \u00e0 votre application. Il n&#8217;est pas con\u00e7u pour \u00eatre valid\u00e9 c\u00f4t\u00e9 client, et plusieurs failles document\u00e9es d\u00e9coulent de cette confusion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Concr\u00e8tement, dans le flux que nous construisons, le fournisseur d&#8217;identit\u00e9 (Auth0, Keycloak, Google, Microsoft Entra ID) nous renvoie trois jetons : un ID token pour savoir qui se connecte, un access token pour appeler des API, et un refresh token pour renouveler la session sans redemander le mot de passe. Notre application valide l&#8217;ID token, cr\u00e9e une session locale, et conserve le refresh token de fa\u00e7on s\u00e9curis\u00e9e. Si vous avez d\u00e9j\u00e0 suivi notre tutoriel sur l&#8217;<a href=\"\/fr\/authentification-jwt-nodejs\/\">authentification JWT en Node.js<\/a>, vous reconna\u00eetrez la structure des jetons : OIDC standardise simplement leur \u00e9mission par un tiers de confiance.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pourquoi-oauth-2-1-redefinit-les-bonnes-pratiques-en-2026\">Pourquoi OAuth 2.1 red\u00e9finit les bonnes pratiques en 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.1 ne r\u00e9invente rien. Il consolide quinze ans de correctifs de s\u00e9curit\u00e9 \u00e9pars en une sp\u00e9cification unique, et il rend obligatoires des mesures qui \u00e9taient jusqu&#8217;ici recommand\u00e9es. Trois changements structurent toute impl\u00e9mentation moderne et conditionnent le code de ce tutoriel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Premi\u00e8rement, <strong>PKCE<\/strong> (Proof Key for Code Exchange, RFC 7636) devient obligatoire pour le flux Authorization Code, pour tous les clients, y compris les clients confidentiels c\u00f4t\u00e9 serveur. PKCE emp\u00eache l&#8217;interception du code d&#8217;autorisation : m\u00eame si un attaquant capture le code, il ne peut pas l&#8217;\u00e9changer sans le \u00ab code verifier \u00bb secret g\u00e9n\u00e9r\u00e9 par le client l\u00e9gitime.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Deuxi\u00e8mement, le <strong>grant implicite<\/strong> (qui renvoyait l&#8217;access token directement dans l&#8217;URL) est supprim\u00e9, tout comme le grant \u00ab Resource Owner Password Credentials \u00bb. Ces deux flux exposaient les jetons dans l&#8217;historique du navigateur ou demandaient \u00e0 l&#8217;application de manipuler le mot de passe de l&#8217;utilisateur. D\u00e9sormais, seul le flux Authorization Code avec PKCE est recommand\u00e9 pour les applications web.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Troisi\u00e8mement, la rotation des refresh tokens devient la norme pour les clients publics. La RFC 9700 (Best Current Practice for OAuth 2.0 Security, publi\u00e9e par l&#8217;IETF) formalise la <strong>d\u00e9tection de r\u00e9utilisation<\/strong> : si un ancien refresh token r\u00e9appara\u00eet apr\u00e8s rotation, le serveur r\u00e9voque toute la \u00ab famille \u00bb de jetons. C&#8217;est exactement ce que nous impl\u00e9menterons aux \u00e9tapes 9 et 10. Le contexte europ\u00e9en renforce cette exigence : la directive NIS2, d\u00e9sormais en vigueur, \u00e9tend les obligations de s\u00e9curit\u00e9 \u00e0 entre 15 000 et 18 000 organisations en France selon la transposition nationale, et l&#8217;authentification r\u00e9sistante au hame\u00e7onnage figure au c\u0153ur des recommandations de l&#8217;ANSSI et de l&#8217;ENISA.<\/p>\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, v\u00e9rifiez que votre environnement correspond aux versions ci-dessous. La biblioth\u00e8que <code>openid-client<\/code> a connu une r\u00e9\u00e9criture majeure en version 6 : elle est d\u00e9sormais \u00ab ESM-first \u00bb et s&#8217;appuie sur les standards web (Fetch, WebCrypto). Le code de ce tutoriel ne fonctionne pas avec les anciennes versions 5.x dont l&#8217;API diff\u00e8re totalement.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Composant<\/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>Runtime, support jusqu&#8217;en avril 2028<\/td><\/tr><tr><td>Express<\/td><td>5.2.1<\/td><td>Serveur HTTP et routage<\/td><\/tr><tr><td>openid-client<\/td><td>6.8.4<\/td><td>D\u00e9couverte OIDC, PKCE, \u00e9change de jetons<\/td><\/tr><tr><td>jose<\/td><td>6.2.3<\/td><td>V\u00e9rification de signature JWT\/ID token<\/td><\/tr><tr><td>cookie-session<\/td><td>2.1.1<\/td><td>Session chiffr\u00e9e c\u00f4t\u00e9 cookie<\/td><\/tr><tr><td>dotenv<\/td><td>latest<\/td><td>Chargement des variables d&#8217;environnement<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">C\u00f4t\u00e9 connaissances, vous devez \u00eatre \u00e0 l&#8217;aise avec JavaScript asynchrone (async\/await), les modules ES (<code>import<\/code>\/<code>export<\/code>) et les bases de HTTP. Il vous faut aussi un compte chez un fournisseur OIDC. Ce tutoriel utilise une configuration g\u00e9n\u00e9rique compatible avec Auth0, Keycloak, Okta ou Microsoft Entra ID. Enfin, ex\u00e9cutez tout en HTTPS d\u00e8s que possible : sans TLS, les cookies de session et les jetons circulent en clair. Notre guide pour obtenir un <a href=\"\/fr\/certificat-ssl-certbot-nginx\/\">certificat SSL gratuit avec Certbot<\/a> couvre cette mise en place, et notre explication sur <a href=\"\/fr\/https-et-tls\/\">HTTPS et TLS<\/a> d\u00e9taille pourquoi ce socle est non n\u00e9gociable.<\/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 r\u00e9pertoire et initialisez le projet. Le point essentiel : d\u00e9clarez <code>\"type\": \"module\"<\/code> dans le <code>package.json<\/code>, car <code>openid-client<\/code> 6 est distribu\u00e9 exclusivement en modules ES. Sans cette ligne, les <code>import<\/code> \u00e9choueront avec une erreur <code>ERR_REQUIRE_ESM<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir oauth2-oidc-nodejs &amp;&amp; cd oauth2-oidc-nodejs\nnpm init -y\nnpm install express@5.2.1 openid-client@6.8.4 jose@6.2.3 cookie-session@2.1.1 dotenv\n\n# Activer les modules ES\nnpm pkg set type=\"module\"\nnpm pkg set engines.node=\"&gt;=24.0.0\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez ensuite un fichier <code>.env<\/code> \u00e0 la racine. Ne le versionnez jamais : ajoutez-le imm\u00e9diatement \u00e0 votre <code>.gitignore<\/code>. Les valeurs proviennent de la console de votre fournisseur d&#8217;identit\u00e9, dans la section \u00ab Applications \u00bb o\u00f9 vous enregistrez un client web.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nOIDC_ISSUER=https:\/\/votre-tenant.eu.auth0.com\nOIDC_CLIENT_ID=AbCdEf123456\nOIDC_CLIENT_SECRET=votre_secret_client\nOIDC_REDIRECT_URI=https:\/\/localhost:3000\/callback\nSESSION_KEY=une_cle_aleatoire_de_32_octets_minimum\nPORT=3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">G\u00e9n\u00e9rez la <code>SESSION_KEY<\/code> avec une vraie source d&#8217;entropie, jamais une cha\u00eene devin\u00e9e. La commande <code>node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"<\/code> produit une cl\u00e9 de 32 octets adapt\u00e9e au chiffrement des cookies de session.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-2-enregistrer-lapplication-chez-le-fournisseur-didentite\">\u00c9tape 2 : enregistrer l&#8217;application chez le fournisseur d&#8217;identit\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dans la console de votre fournisseur OIDC, cr\u00e9ez une application de type \u00ab Regular Web Application \u00bb (client confidentiel). Trois r\u00e9glages sont critiques et causent la plupart des erreurs de d\u00e9butant.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Allowed Callback URLs<\/strong> : renseignez exactement <code>https:\/\/localhost:3000\/callback<\/code>. La moindre diff\u00e9rence (slash final, http au lieu de https, port absent) d\u00e9clenche une erreur <code>redirect_uri_mismatch<\/code>.<\/li><li><strong>Grant Types<\/strong> : activez uniquement \u00ab Authorization Code \u00bb et \u00ab Refresh Token \u00bb. D\u00e9sactivez \u00ab Implicit \u00bb : il est supprim\u00e9 par OAuth 2.1.<\/li><li><strong>Refresh Token Rotation<\/strong> : activez la rotation si votre fournisseur le propose (Auth0, Keycloak). D\u00e9finissez aussi une dur\u00e9e de vie absolue pour limiter la fen\u00eatre d&#8217;exploitation en cas de vol.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Notez l&#8217;\u00ab Issuer URL \u00bb. C&#8217;est la racine \u00e0 partir de laquelle <code>openid-client<\/code> d\u00e9couvrira automatiquement tous les endpoints (autorisation, jeton, JWKS) via le document <code>\/.well-known\/openid-configuration<\/code>. Cette d\u00e9couverte automatique \u00e9vite de coder en dur des URL qui changent selon le fournisseur.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-3-configurer-openid-client-6-et-la-decouverte-oidc\">\u00c9tape 3 : configurer openid-client 6 et la d\u00e9couverte OIDC<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez un fichier <code>oidc.js<\/code> qui centralise la configuration. La fonction <code>discovery()<\/code> de la version 6 r\u00e9cup\u00e8re les m\u00e9tadonn\u00e9es du fournisseur et construit un objet <code>Configuration<\/code> r\u00e9utilisable. C&#8217;est le point d&#8217;entr\u00e9e de toute l&#8217;API moderne de la biblioth\u00e8que.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ oidc.js\nimport * as client from 'openid-client';\nimport 'dotenv\/config';\n\nlet config;\n\nexport async function getConfig() {\n  if (config) return config;\n\n  const issuer = new URL(process.env.OIDC_ISSUER);\n\n  \/\/ Decouverte automatique via \/.well-known\/openid-configuration\n  config = await client.discovery(\n    issuer,\n    process.env.OIDC_CLIENT_ID,\n    process.env.OIDC_CLIENT_SECRET\n  );\n\n  return config;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le m\u00e9canisme de d\u00e9couverte t\u00e9l\u00e9charge aussi l&#8217;URL du JWKS (JSON Web Key Set), l&#8217;ensemble des cl\u00e9s publiques qui serviront \u00e0 v\u00e9rifier la signature de l&#8217;ID token \u00e0 l&#8217;\u00e9tape 7. La biblioth\u00e8que met ces cl\u00e9s en cache et g\u00e8re leur rotation automatiquement. Vous n&#8217;avez donc jamais \u00e0 manipuler de certificat manuellement, contrairement \u00e0 une v\u00e9rification de <a href=\"\/fr\/signatures-numeriques\/\">signature num\u00e9rique<\/a> que l&#8217;on coderait de z\u00e9ro.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-4-generer-le-code-verifier-et-le-code-challenge-pkce\">\u00c9tape 4 : g\u00e9n\u00e9rer le code verifier et le code challenge (PKCE)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE repose sur un secret \u00e9ph\u00e9m\u00e8re. Le client g\u00e9n\u00e8re un <strong>code verifier<\/strong> (cha\u00eene al\u00e9atoire), en d\u00e9rive un <strong>code challenge<\/strong> par hachage SHA-256, envoie le challenge au fournisseur lors de la redirection, puis prouve son identit\u00e9 en r\u00e9v\u00e9lant le verifier au moment de l&#8217;\u00e9change du code. Un attaquant qui intercepte le code d&#8217;autorisation ne poss\u00e8de pas le verifier et ne peut donc rien en faire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ajoutez aussi un param\u00e8tre <code>state<\/code> (anti-CSRF) et un <code>nonce<\/code> (anti-rejeu de l&#8217;ID token). La biblioth\u00e8que fournit des g\u00e9n\u00e9rateurs s\u00fbrs pour chacun. Ces valeurs doivent \u00eatre stock\u00e9es dans la session le temps de l&#8217;aller-retour.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth-helpers.js\nimport * as client from 'openid-client';\n\nexport function createPkcePair() {\n  const code_verifier = client.randomPKCECodeVerifier();\n  return code_verifier;\n}\n\nexport async function buildAuthParams(config, code_verifier) {\n  const code_challenge =\n    await client.calculatePKCECodeChallenge(code_verifier);\n\n  return {\n    redirect_uri: process.env.OIDC_REDIRECT_URI,\n    scope: 'openid profile email offline_access',\n    code_challenge,\n    code_challenge_method: 'S256',\n    state: client.randomState(),\n    nonce: client.randomNonce(),\n  };\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le scope <code>offline_access<\/code> demande explicitement un refresh token : sans lui, le fournisseur n&#8217;en \u00e9met pas, et la rotation des \u00e9tapes 9 et 10 n&#8217;aurait aucun objet. Le <code>code_challenge_method<\/code> doit valoir <code>S256<\/code> (SHA-256) et jamais <code>plain<\/code>, qui n&#8217;apporte aucune protection r\u00e9elle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-5-construire-la-route-de-connexion-login\">\u00c9tape 5 : construire la route de connexion (\/login)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Assemblons le serveur Express. La route <code>\/login<\/code> g\u00e9n\u00e8re le verifier PKCE, le stocke en session avec le state et le nonce, puis redirige l&#8217;utilisateur vers l&#8217;endpoint d&#8217;autorisation du fournisseur. C&#8217;est le d\u00e9part du flux Authorization Code.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js\nimport express from 'express';\nimport cookieSession from 'cookie-session';\nimport * as client from 'openid-client';\nimport { getConfig } from '.\/oidc.js';\nimport { createPkcePair, buildAuthParams } from '.\/auth-helpers.js';\nimport 'dotenv\/config';\n\nconst app = express();\n\napp.use(cookieSession({\n  name: 'sid',\n  keys: [process.env.SESSION_KEY],\n  maxAge: 24 * 60 * 60 * 1000, \/\/ 24 h\n  httpOnly: true,\n  secure: true,\n  sameSite: 'lax',\n}));\n\napp.get('\/login', async (req, res) =&gt; {\n  const config = await getConfig();\n  const code_verifier = createPkcePair();\n  const params = await buildAuthParams(config, code_verifier);\n\n  \/\/ Stockage temporaire pour valider le retour\n  req.session.code_verifier = code_verifier;\n  req.session.state = params.state;\n  req.session.nonce = params.nonce;\n\n  const authUrl = client.buildAuthorizationUrl(config, params);\n  res.redirect(authUrl.href);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notez les options du cookie de session : <code>httpOnly<\/code> bloque l&#8217;acc\u00e8s JavaScript (protection contre le vol par XSS), <code>secure<\/code> impose HTTPS, et <code>sameSite: 'lax'<\/code> limite l&#8217;envoi du cookie sur des requ\u00eates cross-site, ce qui contre une grande partie des attaques CSRF. Ces trois r\u00e9glages sont le minimum pour un cookie de session en 2026.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-6-gerer-le-callback-et-echanger-le-code\">\u00c9tape 6 : g\u00e9rer le callback et \u00e9changer le code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Apr\u00e8s authentification, le fournisseur redirige l&#8217;utilisateur vers <code>\/callback<\/code> avec le code d&#8217;autorisation et le state. La fonction <code>authorizationCodeGrant()<\/code> v\u00e9rifie le state, \u00e9change le code contre les jetons en pr\u00e9sentant le code verifier, et valide automatiquement la signature et le nonce de l&#8217;ID token. C&#8217;est l&#8217;\u00e9tape la plus dense du flux.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app.get('\/callback', async (req, res) =&gt; {\n  const config = await getConfig();\n  const currentUrl = new URL(\n    req.originalUrl, `https:\/\/localhost:${process.env.PORT}`\n  );\n\n  try {\n    const tokens = await client.authorizationCodeGrant(\n      config,\n      currentUrl,\n      {\n        pkceCodeVerifier: req.session.code_verifier,\n        expectedState: req.session.state,\n        expectedNonce: req.session.nonce,\n      }\n    );\n\n    const claims = tokens.claims();\n\n    \/\/ Nettoyage des valeurs temporaires\n    delete req.session.code_verifier;\n    delete req.session.state;\n    delete req.session.nonce;\n\n    \/\/ Creation de la session applicative\n    req.session.user = {\n      sub: claims.sub,\n      email: claims.email,\n      name: claims.name,\n    };\n    req.session.refresh_token = tokens.refresh_token;\n    req.session.access_token = tokens.access_token;\n    req.session.token_family = client.randomState();\n\n    res.redirect('\/profile');\n  } catch (err) {\n    console.error('Echec du callback OIDC :', err.message);\n    res.status(401).send('Authentification echouee');\n  }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La m\u00e9thode <code>tokens.claims()<\/code> renvoie le contenu d\u00e9j\u00e0 valid\u00e9 de l&#8217;ID token : inutile de rev\u00e9rifier la signature manuellement ici, la biblioth\u00e8que l&#8217;a fait. Le champ <code>token_family<\/code> que nous ajoutons servira \u00e0 la d\u00e9tection de r\u00e9utilisation : tous les refresh tokens issus d&#8217;une m\u00eame connexion partagent cet identifiant de famille.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-7-verifier-un-id-token-manuellement-avec-jose\">\u00c9tape 7 : v\u00e9rifier un ID token manuellement avec jose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dans la plupart des cas, <code>openid-client<\/code> valide l&#8217;ID token pour vous. Mais vous aurez besoin d&#8217;une v\u00e9rification manuelle d\u00e8s que vous transmettez un jeton \u00e0 un autre service (microservices) ou que vous recevez un access token au format JWT c\u00f4t\u00e9 API. La biblioth\u00e8que <code>jose<\/code> est l&#8217;outil de r\u00e9f\u00e9rence pour cela. Elle r\u00e9cup\u00e8re les cl\u00e9s publiques du fournisseur via son JWKS et v\u00e9rifie la signature, l&#8217;\u00e9metteur, l&#8217;audience et l&#8217;expiration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ verify-token.js\nimport * as jose from 'jose';\n\nconst JWKS = jose.createRemoteJWKSet(\n  new URL(`${process.env.OIDC_ISSUER}\/.well-known\/jwks.json`)\n);\n\nexport async function verifyIdToken(idToken) {\n  const { payload } = await jose.jwtVerify(idToken, JWKS, {\n    issuer: process.env.OIDC_ISSUER,\n    audience: process.env.OIDC_CLIENT_ID,\n    maxTokenAge: '1h',\n  });\n  return payload;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>createRemoteJWKSet<\/code> met les cl\u00e9s en cache et g\u00e8re leur rotation : si le fournisseur change de cl\u00e9 de signature, <code>jose<\/code> r\u00e9cup\u00e8re automatiquement la nouvelle. Ne d\u00e9sactivez jamais la v\u00e9rification de l&#8217;audience ni de l&#8217;\u00e9metteur. Un jeton valide \u00e9mis pour une autre application ne doit jamais \u00eatre accept\u00e9 par la v\u00f4tre, faute de quoi vous ouvrez une faille de \u00ab token substitution \u00bb.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-8-securiser-la-session-par-cookie-chiffre\">\u00c9tape 8 : s\u00e9curiser la session par cookie chiffr\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le choix du stockage de session est une d\u00e9cision de s\u00e9curit\u00e9. Deux approches dominent. La premi\u00e8re, le cookie de session sign\u00e9 et chiffr\u00e9 (notre <code>cookie-session<\/code>), stocke les donn\u00e9es chez le client : simple, sans \u00e9tat serveur, mais limit\u00e9 en taille et difficile \u00e0 r\u00e9voquer imm\u00e9diatement. La seconde, la session c\u00f4t\u00e9 serveur (Redis, base de donn\u00e9es), permet une r\u00e9vocation instantan\u00e9e mais demande une infrastructure.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>R\u00e9glage cookie<\/th><th>Valeur recommand\u00e9e<\/th><th>Menace contr\u00e9e<\/th><\/tr><\/thead><tbody><tr><td>httpOnly<\/td><td>true<\/td><td>Vol de cookie via XSS<\/td><\/tr><tr><td>secure<\/td><td>true<\/td><td>Interception sur HTTP en clair<\/td><\/tr><tr><td>sameSite<\/td><td>lax (ou strict)<\/td><td>CSRF cross-site<\/td><\/tr><tr><td>maxAge<\/td><td>court (1 \u00e0 24 h)<\/td><td>Fen\u00eatre de session vol\u00e9e<\/td><\/tr><tr><td>domain\/path<\/td><td>le plus restrictif<\/td><td>Fuite vers sous-domaines<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pour une application sensible (banque, sant\u00e9, secteur soumis \u00e0 NIS2), privil\u00e9giez la session serveur avec Redis : elle seule permet de r\u00e9voquer une session en cas de compromission d\u00e9tect\u00e9e. Pour ce tutoriel, le cookie chiffr\u00e9 suffit \u00e0 illustrer le flux. Quelle que soit l&#8217;option, ne stockez jamais le refresh token dans un cookie accessible au JavaScript, ni dans le <code>localStorage<\/code> : ce sont les cibles favorites des attaques XSS. La gestion des secrets reste un sujet \u00e0 part enti\u00e8re, compl\u00e9mentaire de la <a href=\"\/fr\/securite-des-mots-de-passe\/\">s\u00e9curit\u00e9 des mots de passe<\/a> c\u00f4t\u00e9 utilisateur.<\/p>\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\">Voici le c\u0153ur de la s\u00e9curit\u00e9 moderne. Quand l&#8217;access token expire (typiquement apr\u00e8s 15 minutes \u00e0 1 heure), le client utilise le refresh token pour en obtenir un nouveau sans redemander le mot de passe. Avec la <strong>rotation<\/strong>, le fournisseur renvoie aussi un nouveau refresh token et invalide l&#8217;ancien. Chaque renouvellement remplace donc le jeton pr\u00e9c\u00e9dent.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ refresh.js\nimport * as client from 'openid-client';\nimport { getConfig } from '.\/oidc.js';\n\nexport async function refreshSession(req) {\n  const config = await getConfig();\n  const oldRefreshToken = req.session.refresh_token;\n\n  if (!oldRefreshToken) {\n    throw new Error('Aucun refresh token en session');\n  }\n\n  const tokens = await client.refreshTokenGrant(\n    config,\n    oldRefreshToken\n  );\n\n  \/\/ Rotation : on remplace l'ancien jeton par le nouveau\n  req.session.access_token = tokens.access_token;\n  if (tokens.refresh_token) {\n    req.session.refresh_token = tokens.refresh_token;\n  }\n\n  return tokens;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le point cl\u00e9 : apr\u00e8s l&#8217;appel, l&#8217;ancien refresh token ne doit plus jamais servir. Si votre fournisseur a la rotation activ\u00e9e (configur\u00e9e \u00e0 l&#8217;\u00e9tape 2), il invalide automatiquement l&#8217;ancien jeton c\u00f4t\u00e9 serveur. Votre code doit refl\u00e9ter cette logique en rempla\u00e7ant imm\u00e9diatement la valeur en session. Un refresh token qui vit ind\u00e9finiment \u00e9quivaut \u00e0 un mot de passe permanent volable : c&#8217;est pr\u00e9cis\u00e9ment ce que la rotation \u00e9limine.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-10-detecter-la-reutilisation-de-token-reuse-detection\">\u00c9tape 10 : d\u00e9tecter la r\u00e9utilisation de token (reuse detection)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La rotation seule ne suffit pas. Imaginez qu&#8217;un attaquant vole un refresh token et l&#8217;utilise avant la victime. Le serveur \u00e9met un nouveau jeton \u00e0 l&#8217;attaquant et invalide l&#8217;ancien. Quand la victime l\u00e9gitime tente ensuite d&#8217;utiliser <em>son<\/em> jeton (l&#8217;ancien, d\u00e9sormais invalide), le serveur d\u00e9tecte qu&#8217;un jeton d\u00e9j\u00e0 tourn\u00e9 r\u00e9appara\u00eet. C&#8217;est le signal d&#8217;un vol. La r\u00e9ponse correcte : r\u00e9voquer toute la <strong>famille<\/strong> de jetons, for\u00e7ant attaquant et victime \u00e0 se reconnecter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">C\u00f4t\u00e9 application, vous tenez un registre des familles de jetons (en base ou en Redis). Voici une impl\u00e9mentation minimale avec une Map en m\u00e9moire, \u00e0 remplacer par un stockage persistant en production.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ reuse-detection.js\nconst tokenFamilies = new Map(); \/\/ famille -&gt; { active: Set, revoked: bool }\n\nexport function registerToken(family, jti) {\n  if (!tokenFamilies.has(family)) {\n    tokenFamilies.set(family, { active: new Set(), revoked: false });\n  }\n  tokenFamilies.get(family).active.add(jti);\n}\n\nexport function rotateToken(family, oldJti, newJti) {\n  const f = tokenFamilies.get(family);\n  if (!f || f.revoked) return false;\n\n  \/\/ Reutilisation d'un jeton deja retire = vol probable\n  if (!f.active.has(oldJti)) {\n    f.revoked = true;        \/\/ on revoque toute la famille\n    f.active.clear();\n    console.warn(`ALERTE : reutilisation detectee, famille ${family} revoquee`);\n    return false;\n  }\n\n  f.active.delete(oldJti);\n  f.active.add(newJti);\n  return true;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En pratique, beaucoup de fournisseurs (Auth0, Keycloak, Okta) g\u00e8rent eux-m\u00eames la d\u00e9tection de r\u00e9utilisation quand vous activez la rotation. Impl\u00e9menter cette logique c\u00f4t\u00e9 application reste utile pour les architectures o\u00f9 vous \u00e9mettez vos propres refresh tokens, ou pour journaliser les tentatives de vol \u00e0 des fins de surveillance, une exigence courante des audits de conformit\u00e9 europ\u00e9ens.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-11-proteger-les-routes-avec-un-middleware\">\u00c9tape 11 : prot\u00e9ger les routes avec un middleware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un middleware Express v\u00e9rifie qu&#8217;une session valide existe avant d&#8217;autoriser l&#8217;acc\u00e8s aux routes prot\u00e9g\u00e9es. S&#8217;il manque un utilisateur en session, on redirige vers <code>\/login<\/code>. Si l&#8217;access token a expir\u00e9, on tente un renouvellement silencieux via la rotation de l&#8217;\u00e9tape 9.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { refreshSession } from '.\/refresh.js';\n\nasync function requireAuth(req, res, next) {\n  if (!req.session.user) {\n    return res.redirect('\/login');\n  }\n  try {\n    \/\/ Renouvellement silencieux si l'access token est expire\n    if (isAccessTokenExpired(req.session.access_token)) {\n      await refreshSession(req);\n    }\n    next();\n  } catch (err) {\n    req.session = null; \/\/ on detruit la session corrompue\n    res.redirect('\/login');\n  }\n}\n\napp.get('\/profile', requireAuth, (req, res) =&gt; {\n  res.json({\n    message: 'Zone protegee',\n    user: req.session.user,\n  });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ce sch\u00e9ma centralise le contr\u00f4le d&#8217;acc\u00e8s : aucune route m\u00e9tier ne manipule les jetons directement. Pour un contr\u00f4le plus fin (r\u00f4les, permissions), inspectez les claims de l&#8217;ID token ou un access token de type JWT. La d\u00e9fense en profondeur reste la r\u00e8gle : ce middleware compl\u00e8te, sans remplacer, des protections comme le filtrage d&#8217;IP ou un outil tel que <a href=\"\/fr\/fail2ban-proteger-ssh-tutoriel\/\">Fail2ban pour limiter les tentatives<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-12-deconnexion-et-revocation-des-jetons\">\u00c9tape 12 : d\u00e9connexion et r\u00e9vocation des jetons<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Une d\u00e9connexion correcte fait deux choses : elle d\u00e9truit la session locale, et elle r\u00e9voque les jetons aupr\u00e8s du fournisseur via le \u00ab RP-Initiated Logout \u00bb d&#8217;OIDC. D\u00e9truire seulement le cookie laisse le refresh token valide c\u00f4t\u00e9 fournisseur, ce qui est insuffisant.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app.get('\/logout', async (req, res) =&gt; {\n  const config = await getConfig();\n  const refreshToken = req.session.refresh_token;\n\n  \/\/ Revocation cote fournisseur si supporte\n  try {\n    if (refreshToken) {\n      await client.tokenRevocation(config, refreshToken);\n    }\n  } catch (err) {\n    console.error('Revocation impossible :', err.message);\n  }\n\n  \/\/ Destruction de la session locale\n  req.session = null;\n\n  \/\/ Deconnexion globale OIDC\n  const endSessionUrl = client.buildEndSessionUrl(config, {\n    post_logout_redirect_uri: 'https:\/\/localhost:3000\/',\n  });\n  res.redirect(endSessionUrl.href);\n});\n\napp.listen(process.env.PORT, () =&gt; {\n  console.log(`Serveur demarre sur https:\/\/localhost:${process.env.PORT}`);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le projet est maintenant complet : connexion, callback s\u00e9curis\u00e9, sessions, rotation, d\u00e9tection de r\u00e9utilisation, routes prot\u00e9g\u00e9es et d\u00e9connexion globale. Les douze \u00e9tapes forment un flux d&#8217;authentification conforme \u00e0 OAuth 2.1.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"choisir-son-fournisseur-didentite-oidc-en-europe\">Choisir son fournisseur d&#8217;identit\u00e9 OIDC en Europe<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le code de ce tutoriel reste identique quel que soit le fournisseur, gr\u00e2ce \u00e0 la d\u00e9couverte automatique de l&#8217;\u00e9tape 3. Mais le choix du fournisseur engage votre conformit\u00e9, votre budget et votre souverainet\u00e9 des donn\u00e9es, des crit\u00e8res devenus centraux dans le contexte r\u00e9glementaire europ\u00e9en. Quatre options dominent le march\u00e9, chacune avec ses arbitrages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Keycloak<\/strong> est la solution open source de r\u00e9f\u00e9rence, maintenue par la fondation CNCF. Vous l&#8217;auto-h\u00e9bergez, ce qui garantit que les identit\u00e9s et les jetons ne quittent jamais votre infrastructure : un atout d\u00e9cisif pour les organismes publics fran\u00e7ais et les entit\u00e9s soumises \u00e0 NIS2 qui veulent la ma\u00eetrise compl\u00e8te de leurs donn\u00e9es. En contrepartie, vous assumez l&#8217;exploitation, les mises \u00e0 jour de s\u00e9curit\u00e9 et la haute disponibilit\u00e9. La rotation des refresh tokens et la d\u00e9tection de r\u00e9utilisation y sont natives et configurables finement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Auth0<\/strong> (groupe Okta) offre l&#8217;exp\u00e9rience d\u00e9veloppeur la plus fluide, avec une console claire et une documentation abondante. Son offre gratuite suffit aux petits projets. Le point d&#8217;attention en Europe concerne la localisation des donn\u00e9es : choisissez explicitement la r\u00e9gion europ\u00e9enne lors de la cr\u00e9ation du tenant pour rester align\u00e9 avec le RGPD. <strong>Microsoft Entra ID<\/strong> (ex-Azure AD) s&#8217;impose naturellement dans les environnements d\u00e9j\u00e0 investis dans Microsoft 365, avec une int\u00e9gration native aux annuaires d&#8217;entreprise. <strong>ProtonPass, Ory ou Zitadel<\/strong> compl\u00e8tent le paysage pour qui cherche une alternative europ\u00e9enne ou un socle open source moderne.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fournisseur<\/th><th>Mod\u00e8le<\/th><th>H\u00e9bergement<\/th><th>Rotation native<\/th><th>Cas d&#8217;usage type<\/th><\/tr><\/thead><tbody><tr><td>Keycloak<\/td><td>Open source<\/td><td>Auto-h\u00e9berg\u00e9<\/td><td>Oui<\/td><td>Souverainet\u00e9, secteur public<\/td><\/tr><tr><td>Auth0<\/td><td>SaaS (offre gratuite)<\/td><td>Cloud (r\u00e9gion UE possible)<\/td><td>Oui<\/td><td>Startups, prototypage rapide<\/td><\/tr><tr><td>Microsoft Entra ID<\/td><td>SaaS<\/td><td>Cloud Microsoft<\/td><td>Oui<\/td><td>Entreprises Microsoft 365<\/td><\/tr><tr><td>Okta<\/td><td>SaaS<\/td><td>Cloud<\/td><td>Oui<\/td><td>Grandes entreprises B2B<\/td><\/tr><tr><td>Zitadel<\/td><td>Open source<\/td><td>Auto-h\u00e9berg\u00e9 ou cloud UE<\/td><td>Oui<\/td><td>Alternative europ\u00e9enne moderne<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Notre conseil : pour un projet souverain ou soumis \u00e0 NIS2, partez sur Keycloak auto-h\u00e9berg\u00e9 ; pour un produit qui doit aller vite sans \u00e9quipe d\u00e9di\u00e9e \u00e0 l&#8217;identit\u00e9, Auth0 ou Zitadel en r\u00e9gion europ\u00e9enne offrent le meilleur compromis. Dans tous les cas, v\u00e9rifiez que le fournisseur supporte PKCE obligatoire, la rotation des refresh tokens et le RP-Initiated Logout, les trois piliers de notre impl\u00e9mentation. Un fournisseur qui ne propose pas la rotation vous oblige \u00e0 \u00e9mettre vos propres refresh tokens, un travail bien plus risqu\u00e9.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"tester-le-flux-complet-exemples-de-sortie\">Tester le flux complet : exemples de sortie<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Lancez le serveur avec <code>node server.js<\/code> et ouvrez <code>https:\/\/localhost:3000\/login<\/code>. Vous \u00eates redirig\u00e9 vers le fournisseur, vous vous authentifiez, puis vous revenez sur <code>\/profile<\/code>. Voici la sortie attendue de la route prot\u00e9g\u00e9e apr\u00e8s une connexion r\u00e9ussie.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -sk https:\/\/localhost:3000\/profile -b \"sid=...\"\n{\n  \"message\": \"Zone protegee\",\n  \"user\": {\n    \"sub\": \"auth0|6612a8f3c9e1b2\",\n    \"email\": \"marie.dupont@example.fr\",\n    \"name\": \"Marie Dupont\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pour v\u00e9rifier la rotation, d\u00e9clenchez un renouvellement et observez les journaux. Un refresh r\u00e9ussi affiche le remplacement du jeton ; une tentative de r\u00e9utilisation d\u00e9clenche l&#8217;alerte de l&#8217;\u00e9tape 10.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node server.js\nServeur demarre sur https:\/\/localhost:3000\n[refresh] access_token renouvele, nouveau refresh_token emis\n[refresh] access_token renouvele, nouveau refresh_token emis\nALERTE : reutilisation detectee, famille a1b2c3d4 revoquee<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La derni\u00e8re ligne est le comportement de s\u00e9curit\u00e9 recherch\u00e9 : un ancien refresh token r\u00e9appara\u00eet, le syst\u00e8me le traite comme un vol et invalide toute la famille. Dans un test l\u00e9gitime, vous ne devez jamais voir cette alerte.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"6-pieges-courants-a-eviter\">6 pi\u00e8ges courants \u00e0 \u00e9viter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ces erreurs reviennent dans presque toutes les premi\u00e8res impl\u00e9mentations. Les conna\u00eetre \u00e0 l&#8217;avance vous \u00e9pargne des heures de d\u00e9bogage et, surtout, des failles silencieuses.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Authentifier l&#8217;utilisateur avec l&#8217;access token.<\/strong> L&#8217;access token est destin\u00e9 aux API, pas \u00e0 votre application. Utilisez toujours l&#8217;ID token (et ses claims valid\u00e9s) pour identifier qui se connecte.<\/li><li><strong>Stocker le refresh token c\u00f4t\u00e9 navigateur.<\/strong> Ni <code>localStorage<\/code>, ni cookie accessible au JavaScript. Le refresh token reste c\u00f4t\u00e9 serveur ou dans un cookie <code>httpOnly<\/code> chiffr\u00e9.<\/li><li><strong>Oublier la validation du state et du nonce.<\/strong> Sans state, vous \u00eates vuln\u00e9rable au CSRF sur le callback. Sans nonce, l&#8217;ID token peut \u00eatre rejou\u00e9.<\/li><li><strong>Utiliser <code>code_challenge_method=plain<\/code>.<\/strong> Seul <code>S256<\/code> apporte une protection r\u00e9elle. La m\u00e9thode <code>plain<\/code> transmet le verifier en clair.<\/li><li><strong>Ne pas demander le scope <code>offline_access<\/code>.<\/strong> Sans lui, aucun refresh token n&#8217;est \u00e9mis, et votre logique de rotation tourne \u00e0 vide.<\/li><li><strong>Faire confiance \u00e0 un ID token sans v\u00e9rifier l&#8217;audience.<\/strong> Un jeton valide \u00e9mis pour une autre application ne doit jamais \u00eatre accept\u00e9 par la v\u00f4tre.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"depannage-8-erreurs-frequentes-et-leurs-solutions\">D\u00e9pannage : 8 erreurs fr\u00e9quentes et leurs solutions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voici les messages d&#8217;erreur que vous rencontrerez le plus souvent, avec leur cause r\u00e9elle et la correction \u00e0 appliquer.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Erreur<\/th><th>Cause probable<\/th><th>Solution<\/th><\/tr><\/thead><tbody><tr><td>redirect_uri_mismatch<\/td><td>URL de callback non identique \u00e0 celle enregistr\u00e9e<\/td><td>V\u00e9rifier slash final, http\/https et port \u00e0 l&#8217;identique<\/td><\/tr><tr><td>ERR_REQUIRE_ESM<\/td><td><code>type: module<\/code> absent du package.json<\/td><td>Ex\u00e9cuter <code>npm pkg set type=\"module\"<\/code><\/td><\/tr><tr><td>invalid_grant<\/td><td>Code d\u00e9j\u00e0 utilis\u00e9 ou expir\u00e9 (dur\u00e9e ~30 s)<\/td><td>Ne pas rejouer un callback ; relancer \/login<\/td><\/tr><tr><td>PKCE verification failed<\/td><td>code_verifier perdu entre \/login et \/callback<\/td><td>V\u00e9rifier que la session persiste (cookie secure en HTTPS)<\/td><\/tr><tr><td>invalid_client<\/td><td>CLIENT_ID ou CLIENT_SECRET incorrect<\/td><td>Recopier les valeurs depuis la console du fournisseur<\/td><\/tr><tr><td>Pas de refresh_token re\u00e7u<\/td><td>Scope offline_access manquant<\/td><td>Ajouter <code>offline_access<\/code> au scope<\/td><\/tr><tr><td>JWKS could not be fetched<\/td><td>Issuer mal form\u00e9 ou r\u00e9seau bloqu\u00e9<\/td><td>Tester l&#8217;URL \/.well-known\/openid-configuration<\/td><\/tr><tr><td>Signature verification failed<\/td><td>Audience ou issuer non concordant<\/td><td>Aligner audience sur CLIENT_ID et issuer sur OIDC_ISSUER<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pour les erreurs de session (PKCE verification failed en particulier), le coupable est presque toujours le cookie : en d\u00e9veloppement local sans HTTPS, un cookie <code>secure: true<\/code> n&#8217;est pas envoy\u00e9, donc le code_verifier dispara\u00eet entre les deux requ\u00eates. La solution propre est d&#8217;activer HTTPS en local, pas de d\u00e9sactiver <code>secure<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"astuces-avancees-pour-la-production\">Astuces avanc\u00e9es pour la production<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le projet fonctionne, mais une mise en production exige des renforcements suppl\u00e9mentaires. Voici les optimisations qui distinguent un prototype d&#8217;une application pr\u00eate pour un audit NIS2.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Migrez vers une session serveur (Redis).<\/strong> Elle permet la r\u00e9vocation instantan\u00e9e, indispensable en cas d&#8217;incident. Le cookie ne contient alors qu&#8217;un identifiant de session opaque.<\/li><li><strong>R\u00e9duisez la dur\u00e9e de vie des access tokens \u00e0 15 minutes.<\/strong> Combin\u00e9e \u00e0 la rotation des refresh tokens, cette fen\u00eatre courte limite drastiquement l&#8217;impact d&#8217;un vol.<\/li><li><strong>Activez DPoP ou mTLS pour les jetons li\u00e9s au client.<\/strong> Ces m\u00e9canismes lient le jeton \u00e0 une cl\u00e9 du client, rendant un jeton vol\u00e9 inutilisable ailleurs.<\/li><li><strong>Journalisez chaque r\u00e9vocation de famille.<\/strong> Une r\u00e9utilisation d\u00e9tect\u00e9e est un signal d&#8217;incident \u00e0 remonter \u00e0 votre SIEM.<\/li><li><strong>Verrouillez les en-t\u00eates HTTP avec helmet.<\/strong> CSP stricte, HSTS et X-Content-Type-Options r\u00e9duisent la surface d&#8217;attaque XSS qui menacerait vos sessions.<\/li><li><strong>Limitez le d\u00e9bit sur \/login et \/callback.<\/strong> Un rate limiting emp\u00eache le bourrage de codes et les attaques par force brute sur le flux d&#8217;autorisation.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Enfin, gardez vos d\u00e9pendances \u00e0 jour. L&#8217;incident du ver Shai-Hulud, qui a compromis 18 paquets npm totalisant plus de 2,6 milliards de t\u00e9l\u00e9chargements hebdomadaires en septembre 2025 avant qu&#8217;une variante n&#8217;atteigne environ 700 paquets en novembre, rappelle que la cha\u00eene d&#8217;approvisionnement logicielle est une cible. \u00c9pinglez vos versions, v\u00e9rifiez les signatures et auditez r\u00e9guli\u00e8rement votre arbre de d\u00e9pendances.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"questions-frequentes-sur-oauth2-et-openid-connect-en-node-js\">Questions fr\u00e9quentes sur OAuth2 et OpenID Connect en Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faut-il-pkce-pour-un-client-confidentiel-cote-serveur\">Faut-il PKCE pour un client confidentiel c\u00f4t\u00e9 serveur ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Oui. OAuth 2.1 rend PKCE obligatoire pour tous les clients utilisant le flux Authorization Code, y compris les clients confidentiels avec secret. PKCE prot\u00e8ge contre l&#8217;interception du code, une menace ind\u00e9pendante du secret client. Il n&#8217;y a aucune raison de s&#8217;en passer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quelle-est-la-difference-entre-access-token-et-id-token\">Quelle est la diff\u00e9rence entre access token et ID token ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;access token autorise l&#8217;acc\u00e8s \u00e0 une API : votre application le transmet, sans le lire, \u00e0 un service tiers. L&#8217;ID token prouve l&#8217;identit\u00e9 de l&#8217;utilisateur \u00e0 votre application : il est destin\u00e9 \u00e0 \u00eatre lu et valid\u00e9 par vous. Confondre les deux est la faille d&#8217;authentification OIDC la plus r\u00e9pandue.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"openid-client-ou-passport-js-que-choisir-en-2026\">openid-client ou Passport.js : que choisir en 2026 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>openid-client<\/code> est certifi\u00e9 par l&#8217;OpenID Foundation et impl\u00e9mente fid\u00e8lement les sp\u00e9cifications, PKCE et rotation incluses. Passport.js reste populaire mais sa strat\u00e9gie OIDC ajoute une couche d&#8217;abstraction qui masque des d\u00e9tails de s\u00e9curit\u00e9. Pour une impl\u00e9mentation OIDC stricte et maintenue, <code>openid-client<\/code> 6 est le choix de r\u00e9f\u00e9rence.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ou-stocker-le-refresh-token-en-toute-securite\">O\u00f9 stocker le refresh token en toute s\u00e9curit\u00e9 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">C\u00f4t\u00e9 serveur : en base de donn\u00e9es ou Redis chiffr\u00e9, jamais expos\u00e9 au navigateur. Si vous devez le conserver c\u00f4t\u00e9 client, utilisez exclusivement un cookie <code>httpOnly<\/code>, <code>secure<\/code> et <code>sameSite<\/code>. Bannissez le <code>localStorage<\/code>, accessible au moindre script XSS.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"la-rotation-des-refresh-tokens-suffit-elle-contre-le-vol\">La rotation des refresh tokens suffit-elle contre le vol ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Non, seule. La rotation doit s&#8217;accompagner de la d\u00e9tection de r\u00e9utilisation : c&#8217;est elle qui transforme un ancien jeton r\u00e9apparu en signal d&#8217;alerte et r\u00e9voque toute la famille. La rotation limite la dur\u00e9e de vie ; la d\u00e9tection neutralise l&#8217;attaquant. Les deux sont compl\u00e9mentaires.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ce-flux-est-il-conforme-aux-exigences-nis2\">Ce flux est-il conforme aux exigences NIS2 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le flux d\u00e9crit (authentification forte d\u00e9l\u00e9gu\u00e9e, PKCE, rotation, journalisation des incidents) couvre les attentes d&#8217;authentification r\u00e9sistante au hame\u00e7onnage que recommandent l&#8217;ANSSI et l&#8217;ENISA. La conformit\u00e9 NIS2 globale d\u00e9pend toutefois de l&#8217;ensemble de votre dispositif : gestion des acc\u00e8s, surveillance, r\u00e9ponse \u00e0 incident et tests ind\u00e9pendants.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faut-il-verifier-lid-token-si-openid-client-le-fait-deja\">Faut-il v\u00e9rifier l&#8217;ID token si openid-client le fait d\u00e9j\u00e0 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pas dans le callback : la biblioth\u00e8que valide signature, nonce, issuer et audience pour vous. La v\u00e9rification manuelle avec <code>jose<\/code> devient n\u00e9cessaire quand vous propagez un jeton vers un microservice ou recevez un JWT c\u00f4t\u00e9 API, l\u00e0 o\u00f9 aucune validation pr\u00e9alable n&#8217;a eu lieu.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pourquoi-mes-cookies-disparaissent-ils-en-developpement-local\">Pourquoi mes cookies disparaissent-ils en d\u00e9veloppement local ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Parce que <code>secure: true<\/code> emp\u00eache l&#8217;envoi du cookie sur une connexion HTTP non chiffr\u00e9e. La bonne solution est d&#8217;activer HTTPS en local (certificat auto-sign\u00e9 ou mkcert), pas de d\u00e9sactiver l&#8217;option <code>secure<\/code>, qui doit rester active partout.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ressources-et-articles-lies\">Ressources et articles li\u00e9s<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/fr\/authentification-jwt-nodejs\/\">Authentification JWT en Node.js : 12 \u00e9tapes [2026]<\/a><\/li><li><a href=\"\/fr\/signatures-numeriques\/\">Signatures num\u00e9riques : comment le hachage et les cl\u00e9s garantissent l&#8217;authenticit\u00e9<\/a><\/li><li><a href=\"\/fr\/certificat-ssl-certbot-nginx\/\">Certificat SSL gratuit avec Certbot : 11 \u00e9tapes [2026]<\/a><\/li><li><a href=\"\/fr\/https-et-tls\/\">HTTPS et TLS : comment votre connexion est prot\u00e9g\u00e9e<\/a><\/li><li><a href=\"\/fr\/securite-des-mots-de-passe\/\">S\u00e9curit\u00e9 des mots de passe : longueur, hachage et gestionnaires<\/a><\/li><li><a href=\"\/fr\/fail2ban-proteger-ssh-tutoriel\/\">Fail2ban : prot\u00e9ger SSH en 12 \u00e9tapes [2026]<\/a><\/li><li><a href=\"\/fr\/security-hub\/\">S\u00e9curit\u00e9 en ligne : prot\u00e9ger ses donn\u00e9es et ses comptes<\/a><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Pour approfondir les sp\u00e9cifications, consultez les sources de r\u00e9f\u00e9rence : la <a href=\"https:\/\/oauth.net\/2.1\/\" target=\"_blank\" rel=\"noopener\">sp\u00e9cification OAuth 2.1<\/a>, la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" target=\"_blank\" rel=\"noopener\">RFC 7636 sur PKCE<\/a>, la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc9700\" target=\"_blank\" rel=\"noopener\">RFC 9700 (bonnes pratiques de s\u00e9curit\u00e9 OAuth)<\/a>, le guide <a href=\"https:\/\/openid.net\/developers\/how-connect-works\/\" target=\"_blank\" rel=\"noopener\">OpenID Connect de l&#8217;OpenID Foundation<\/a> et le d\u00e9p\u00f4t officiel de <a href=\"https:\/\/github.com\/panva\/openid-client\" target=\"_blank\" rel=\"noopener\">openid-client<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Un identifiant vol\u00e9 reste la porte d&#8217;entr\u00e9e num\u00e9ro un des attaquants. En 2026, d\u00e9l\u00e9guer l&#8217;authentification \u00e0 un fournisseur d&#8217;identit\u00e9 via OAuth2 et OpenID Connect n&#8217;est plus une option avanc\u00e9e, c&#8217;est\u2026<\/p>\n","protected":false},"author":2,"featured_media":118,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-117","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\/117","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=117"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/117\/revisions"}],"predecessor-version":[{"id":119,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/117\/revisions\/119"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media\/118"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media?parent=117"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/categories?post=117"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/tags?post=117"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}