{"id":210,"date":"2026-06-17T20:30:31","date_gmt":"2026-06-17T20:30:31","guid":{"rendered":"https:\/\/shattered.io\/fr\/2026\/06\/17\/oauth2-nodejs\/"},"modified":"2026-06-17T20:32:08","modified_gmt":"2026-06-17T20:32:08","slug":"oauth2-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/fr\/2026\/06\/17\/oauth2-nodejs\/","title":{"rendered":"OAuth2 en Node.js : 12 \u00c9tapes, 30 Min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">OAuth2 s\u00e9curise aujourd&#8217;hui plus de 90 % des applications web modernes, des connexions Google aux API d&#8217;entreprise. Ce tutoriel vous guide pas \u00e0 pas pour impl\u00e9menter OAuth2 avec le flux <strong>Authorization Code + PKCE<\/strong> dans une application Express, int\u00e9grer Google et GitHub comme fournisseurs, g\u00e9rer les refresh tokens et s\u00e9curiser chaque \u00e9tape. Dur\u00e9e estim\u00e9e : 30 minutes. Niveau : interm\u00e9diaire.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pourquoi-oauth2-reste-incontournable-en-2026\">Pourquoi OAuth2 reste incontournable en 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2 (RFC 6749) d\u00e9finit un cadre d&#8217;autorisation d\u00e9l\u00e9gu\u00e9e qui permet \u00e0 une application d&#8217;acc\u00e9der \u00e0 des ressources au nom d&#8217;un utilisateur, sans jamais toucher \u00e0 son mot de passe. En 2026, OpenID Connect (OIDC), la couche d&#8217;identit\u00e9 construite sur OAuth2, propulse l&#8217;authentification unique (SSO) de milliards de connexions quotidiennes chez Google, Microsoft, GitHub et des milliers d&#8217;entreprises SaaS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le projet OAuth 2.1, en cours de finalisation \u00e0 l&#8217;IETF, consolide six ans de correctifs de s\u00e9curit\u00e9 en une seule sp\u00e9cification. La principale \u00e9volution : <strong>PKCE (Proof Key for Code Exchange, RFC 7636) devient obligatoire pour tous les clients publics<\/strong>, y compris les applications web traditionnelles. Cette d\u00e9cision s&#8217;impose parce que l&#8217;interception de codes d&#8217;autorisation reste l&#8217;une des attaques OAuth2 les plus document\u00e9es en 2025.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">C\u00f4t\u00e9 r\u00e9glementaire europ\u00e9en, le RGPD impose une tra\u00e7abilit\u00e9 des acc\u00e8s aux donn\u00e9es personnelles. OAuth2 avec des scopes pr\u00e9cis (lecture seule, \u00e9criture, suppression) permet d&#8217;impl\u00e9menter le principe du moindre privil\u00e8ge directement dans le protocole d&#8217;authentification. Une application qui demande uniquement les scopes n\u00e9cessaires r\u00e9duit la surface d&#8217;exposition en cas de compromission de token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Compar\u00e9 \u00e0 l&#8217;authentification basique (identifiant\/mot de passe), OAuth2 pr\u00e9sente trois avantages d\u00e9cisifs. Premi\u00e8rement, les tokens d&#8217;acc\u00e8s ont une dur\u00e9e de vie courte (15 \u00e0 60 minutes), ce qui limite la fen\u00eatre d&#8217;exploitation apr\u00e8s une fuite. Deuxi\u00e8mement, les refresh tokens permettent de renouveler la session sans demander \u00e0 l&#8217;utilisateur de se r\u00e9-authentifier. Troisi\u00e8mement, la r\u00e9vocation des tokens est instantan\u00e9e c\u00f4t\u00e9 serveur d&#8217;autorisation, sans n\u00e9cessiter de modification c\u00f4t\u00e9 client.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour Node.js, l&#8217;\u00e9cosyst\u00e8me s&#8217;est structur\u00e9 autour de trois biblioth\u00e8ques compl\u00e9mentaires. <strong>openid-client<\/strong> g\u00e8re le protocole OIDC c\u00f4t\u00e9 client (d\u00e9couverte, \u00e9change de tokens, PKCE). <strong>passport<\/strong> orchestre les strat\u00e9gies d&#8217;authentification dans Express. <strong>express-oauth2-jwt-bearer<\/strong> valide les tokens d&#8217;acc\u00e8s entrants sur les API resource servers. Ce tutoriel utilise principalement openid-client pour une impl\u00e9mentation directe du protocole, sans couche d&#8217;abstraction suppl\u00e9mentaire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un point souvent n\u00e9glig\u00e9 : OAuth2 ne remplace pas la protection CSRF sur vos formulaires applicatifs. Le param\u00e8tre <code>state<\/code> prot\u00e8ge le flux OAuth2 lui-m\u00eame, mais vos autres routes POST n\u00e9cessitent toujours une protection CSRF d\u00e9di\u00e9e, couverte dans l&#8217;article <a href=\"\/fr\/csrf-protection-nodejs\/\">CSRF Protection en Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequis-et-versions\">Pr\u00e9requis et versions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de commencer, v\u00e9rifiez que votre environnement correspond aux versions suivantes. Les incompatibilit\u00e9s de version sont la premi\u00e8re cause d&#8217;\u00e9chec lors de l&#8217;impl\u00e9mentation OAuth2 en Node.js.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Outil<\/th><th>Version minimale<\/th><th>V\u00e9rification<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>20.x LTS<\/td><td><code>node --version<\/code><\/td><\/tr><tr><td>npm<\/td><td>10.x<\/td><td><code>npm --version<\/code><\/td><\/tr><tr><td>Express<\/td><td>4.x<\/td><td><code>npm list express<\/code><\/td><\/tr><tr><td>openid-client<\/td><td>5.x<\/td><td><code>npm list openid-client<\/code><\/td><\/tr><tr><td>express-session<\/td><td>1.x<\/td><td><code>npm list express-session<\/code><\/td><\/tr><tr><td>helmet<\/td><td>7.x<\/td><td><code>npm list helmet<\/code><\/td><\/tr><tr><td>Redis<\/td><td>7.x (production)<\/td><td><code>redis-server --version<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Vous aurez \u00e9galement besoin d&#8217;un compte Google Cloud (gratuit) pour cr\u00e9er une application OAuth2 dans la Google API Console, et d&#8217;un compte GitHub pour l&#8217;int\u00e9gration du fournisseur GitHub. Les deux sont accessibles gratuitement en d\u00e9veloppement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Connaissances suppos\u00e9es : bases de JavaScript asynchrone (async\/await), HTTP (requ\u00eates, cookies, en-t\u00eates), et une compr\u00e9hension \u00e9l\u00e9mentaire des JWT. Pour approfondir les JWT avant ce tutoriel, consultez l&#8217;article <a href=\"\/fr\/jwt-authentication-nodejs\/\">JWT Authentication en Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"les-4-flux-oauth2-tableau-comparatif\">Les 4 flux OAuth2 : tableau comparatif<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2 d\u00e9finit plusieurs flux (grant types) adapt\u00e9s \u00e0 diff\u00e9rents contextes. Choisir le mauvais flux est une erreur de conception qui cr\u00e9e des vuln\u00e9rabilit\u00e9s structurelles. Voici les quatre flux principaux et quand les utiliser :<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Flux OAuth2<\/th><th>Cas d&#8217;usage<\/th><th>Client secret<\/th><th>PKCE requis<\/th><th>Refresh token<\/th><\/tr><\/thead><tbody><tr><td>Authorization Code + PKCE<\/td><td>Applications web serveur, SPAs, mobile<\/td><td>Optionnel<\/td><td>Obligatoire (OAuth 2.1)<\/td><td>Oui<\/td><\/tr><tr><td>Authorization Code (legacy)<\/td><td>Applications serveur confidentielles<\/td><td>Obligatoire<\/td><td>Recommand\u00e9<\/td><td>Oui<\/td><\/tr><tr><td>Client Credentials<\/td><td>\u00c9changes machine \u00e0 machine (M2M, API)<\/td><td>Obligatoire<\/td><td>Non applicable<\/td><td>Non<\/td><\/tr><tr><td>Device Authorization<\/td><td>TV connect\u00e9e, CLI, appareils sans navigateur<\/td><td>Optionnel<\/td><td>Non (flux distinct)<\/td><td>Oui<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ce tutoriel impl\u00e9mente le flux <strong>Authorization Code + PKCE<\/strong>, qui convient \u00e0 la majorit\u00e9 des applications web Node.js. Le flux Client Credentials est \u00e9galement abord\u00e9 \u00e0 l&#8217;\u00e9tape 11 pour les cas d&#8217;usage machine \u00e0 machine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE fonctionne en deux temps. Avant la redirection vers le fournisseur d&#8217;identit\u00e9, le client g\u00e9n\u00e8re un <code>code_verifier<\/code> al\u00e9atoire de 43 \u00e0 128 caract\u00e8res, puis calcule un <code>code_challenge<\/code> en appliquant SHA-256 sur ce verifier (m\u00e9thode S256). Lors de l&#8217;\u00e9change du code d&#8217;autorisation contre un token, le client renvoie le <code>code_verifier<\/code> original. Le serveur d&#8217;autorisation recalcule SHA-256(code_verifier) et v\u00e9rifie la correspondance avec le challenge initial. Un attaquant qui intercepte le code d&#8217;autorisation ne peut pas l&#8217;utiliser sans poss\u00e9der le code_verifier qu&#8217;il n&#8217;a jamais vu. C&#8217;est la garantie fondamentale de PKCE.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etapes-1-2-initialiser-le-projet-et-les-dependances\">\u00c9tapes 1-2 : Initialiser le projet et les d\u00e9pendances<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 1 : Cr\u00e9er la structure du projet<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez un nouveau r\u00e9pertoire et initialisez le projet Node.js. La structure ci-dessous s\u00e9pare les routes, les middlewares et la configuration pour faciliter la maintenance et les tests unitaires.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">mkdir oauth2-nodejs-tutorial\ncd oauth2-nodejs-tutorial\nnpm init -y\n\n# Cr\u00e9er la structure de r\u00e9pertoires\nmkdir -p src\/{routes,middleware,config,services}\ntouch src\/app.js src\/config\/oidc.js src\/routes\/auth.js\ntouch src\/middleware\/auth.js src\/services\/tokenService.js\ntouch .env .env.example .gitignore\n\n# Ajouter .env au .gitignore imm\u00e9diatement\necho \".env\" >> .gitignore\necho \"node_modules\/\" >> .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 2 : Installer les d\u00e9pendances<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Installez les paquets n\u00e9cessaires. <code>openid-client<\/code> g\u00e8re le protocole OIDC complet avec PKCE. <code>express-session<\/code> stocke le code_verifier et les tokens c\u00f4t\u00e9 serveur. <code>helmet<\/code> ajoute automatiquement les en-t\u00eates de s\u00e9curit\u00e9 HTTP (Content-Security-Policy, HSTS, X-Frame-Options).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\"># D\u00e9pendances de production\nnpm install express openid-client express-session dotenv helmet\n\n# Pour la production avec Redis (sessions persistantes)\nnpm install connect-redis redis\n\n# D\u00e9pendances de d\u00e9veloppement\nnpm install --save-dev nodemon<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">V\u00e9rifiez l&#8217;installation avec <code>npm list --depth=0<\/code>. Vous devriez voir express, openid-client, express-session, dotenv et helmet dans la liste des d\u00e9pendances directes. Ajoutez le script de d\u00e9marrage dans <code>package.json<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n  \"scripts\": {\n    \"start\": \"node src\/app.js\",\n    \"dev\": \"nodemon src\/app.js\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etapes-3-4-variables-denvironnement-et-enregistrement-google\">\u00c9tapes 3-4 : Variables d&#8217;environnement et enregistrement Google<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 3 : Configurer les variables d&#8217;environnement<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les secrets OAuth2 ne doivent jamais \u00eatre \u00e9crits en dur dans le code source. Cr\u00e9ez le fichier <code>.env<\/code> avec les variables suivantes. G\u00e9n\u00e9rez le SESSION_SECRET avec Node.js pour obtenir une valeur cryptographiquement s\u00fbre de 64 octets.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\"># G\u00e9n\u00e9rer un secret de session s\u00e9curis\u00e9\nnode -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\"># .env\nNODE_ENV=development\nPORT=3000\n\n# Session (remplacez par la valeur g\u00e9n\u00e9r\u00e9e ci-dessus)\nSESSION_SECRET=votre_secret_session_64_octets_aleatoire\n\n# Google OAuth2\nGOOGLE_CLIENT_ID=votre_client_id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=GOCSPX-votre_client_secret\nGOOGLE_REDIRECT_URI=http:\/\/localhost:3000\/auth\/google\/callback\n\n# GitHub OAuth2\nGITHUB_CLIENT_ID=votre_github_client_id\nGITHUB_CLIENT_SECRET=votre_github_client_secret\nGITHUB_REDIRECT_URI=http:\/\/localhost:3000\/auth\/github\/callback\n\n# URL de base de l'application\nAPP_BASE_URL=http:\/\/localhost:3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 4 : Enregistrer l&#8217;application dans Google Cloud Console<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Suivez ces 6 \u00e9tapes sur <a href=\"https:\/\/console.cloud.google.com\/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">console.cloud.google.com<\/a> :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Cr\u00e9ez un nouveau projet ou s\u00e9lectionnez un projet existant dans le menu d\u00e9roulant en haut<\/li>\n\n\n\n<li>Naviguez vers <strong>APIs &amp; Services &gt; OAuth consent screen<\/strong> et configurez le consentement en mode &#8220;External&#8221;<\/li>\n\n\n\n<li>Allez dans <strong>APIs &amp; Services &gt; Credentials<\/strong> et cliquez sur &#8220;Create Credentials &gt; OAuth client ID&#8221;<\/li>\n\n\n\n<li>S\u00e9lectionnez &#8220;Web application&#8221; comme type d&#8217;application<\/li>\n\n\n\n<li>Ajoutez <code>http:\/\/localhost:3000\/auth\/google\/callback<\/code> dans &#8220;Authorized redirect URIs&#8221;<\/li>\n\n\n\n<li>Copiez le Client ID et le Client Secret dans votre fichier <code>.env<\/code><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Pour GitHub, rendez-vous dans <strong>Settings &gt; Developer settings &gt; OAuth Apps &gt; New OAuth App<\/strong>. L&#8217;URL de rappel sera <code>http:\/\/localhost:3000\/auth\/github\/callback<\/code>. En production, toutes les redirect URIs doivent utiliser HTTPS. Consultez l&#8217;article <a href=\"\/fr\/lets-encrypt-nginx-https-tutoriel\/\">Let&#8217;s Encrypt + Nginx : HTTPS en 12 \u00e9tapes<\/a> pour la mise en place.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etapes-5-6-sessions-express-et-client-oidc\">\u00c9tapes 5-6 : Sessions Express et client OIDC<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 5 : Configurer Express avec les sessions et Helmet<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les sessions HTTP stockent le <code>code_verifier<\/code> PKCE et les tokens entre les requ\u00eates. Le cookie de session doit imp\u00e9rativement \u00eatre <code>httpOnly<\/code> pour emp\u00eacher l&#8217;acc\u00e8s depuis JavaScript (protection XSS), <code>Secure<\/code> en production (HTTPS uniquement), et <code>SameSite: lax<\/code> pour une protection CSRF partielle.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ src\/app.js\nrequire('dotenv').config();\nconst express = require('express');\nconst session = require('express-session');\nconst helmet = require('helmet');\nconst authRouter = require('.\/routes\/auth');\nconst { requireAuth, checkTokenExpiry } = require('.\/middleware\/auth');\n\nconst app = express();\n\n\/\/ En-t\u00eates de s\u00e9curit\u00e9 HTTP avec Helmet\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [\"'self'\"],\n      scriptSrc: [\"'self'\"],\n      styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n      imgSrc: [\"'self'\", 'https:\/\/lh3.googleusercontent.com', 'https:\/\/avatars.githubusercontent.com'],\n      connectSrc: [\"'self'\"],\n    },\n  },\n  hsts: {\n    maxAge: 31536000,\n    includeSubDomains: true,\n  },\n}));\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: false }));\n\n\/\/ Configuration des sessions\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  name: 'sid', \/\/ Renommer le cookie pour \u00e9viter le fingerprinting\n  cookie: {\n    secure: process.env.NODE_ENV === 'production',\n    httpOnly: true,\n    sameSite: 'lax',\n    maxAge: 24 * 60 * 60 * 1000, \/\/ 24 heures\n  },\n}));\n\n\/\/ Middleware de renouvellement automatique des tokens\napp.use(checkTokenExpiry);\n\napp.use('\/auth', authRouter);\n\n\/\/ Route prot\u00e9g\u00e9e d'exemple\napp.get('\/dashboard', requireAuth, (req, res) => {\n  res.json({\n    user: req.session.user,\n    provider: req.session.user?.provider,\n  });\n});\n\napp.get('\/', (req, res) => {\n  const user = req.session.user;\n  res.send(`\n    &lt;h1&gt;OAuth2 Demo Node.js&lt;\/h1&gt;\n    ${user\n      ? `&lt;p&gt;Connect\u00e9 : ${user.name} (${user.email})&lt;\/p&gt;\n         &lt;a href=\"\/dashboard\"&gt;Tableau de bord&lt;\/a&gt; |\n         &lt;a href=\"\/auth\/logout\"&gt;D\u00e9connexion&lt;\/a&gt;`\n      : `&lt;a href=\"\/auth\/google\"&gt;Connexion avec Google&lt;\/a&gt; |\n         &lt;a href=\"\/auth\/github\"&gt;Connexion avec GitHub&lt;\/a&gt;`\n    }\n  `);\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.log(`Serveur OAuth2 d\u00e9marr\u00e9 sur http:\/\/localhost:${PORT}`);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 6 : Initialiser le client OIDC avec d\u00e9couverte automatique<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La d\u00e9couverte OIDC (RFC 8414) r\u00e9cup\u00e8re automatiquement la configuration du fournisseur depuis l&#8217;URL <code>\/.well-known\/openid-configuration<\/code>. Pour Google, cette URL est <code>https:\/\/accounts.google.com\/.well-known\/openid-configuration<\/code> et expose tous les endpoints (autorisation, token, userinfo, r\u00e9vocation) ainsi que les algorithmes support\u00e9s. Le pattern singleton garantit une seule d\u00e9couverte au d\u00e9marrage.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ src\/config\/oidc.js\nconst { Issuer } = require('openid-client');\n\nlet googleClient = null;\n\nasync function getGoogleClient() {\n  if (googleClient) return googleClient;\n\n  \/\/ D\u00e9couverte automatique de la configuration Google OIDC\n  const googleIssuer = await Issuer.discover('https:\/\/accounts.google.com');\n\n  console.log('OIDC d\u00e9couverte r\u00e9ussie :', googleIssuer.metadata.issuer);\n\n  googleClient = new googleIssuer.Client({\n    client_id: process.env.GOOGLE_CLIENT_ID,\n    client_secret: process.env.GOOGLE_CLIENT_SECRET,\n    redirect_uris: [process.env.GOOGLE_REDIRECT_URI],\n    response_types: ['code'],\n  });\n\n  return googleClient;\n}\n\n\/\/ Pr\u00e9-charger la configuration au d\u00e9marrage (optionnel mais recommand\u00e9)\nasync function initializeOIDC() {\n  try {\n    await getGoogleClient();\n    console.log('Client OIDC initialis\u00e9 avec succ\u00e8s');\n  } catch (err) {\n    console.error('\u00c9chec initialisation OIDC:', err.message);\n    process.exit(1); \/\/ Arr\u00eater si la d\u00e9couverte \u00e9choue\n  }\n}\n\nmodule.exports = { getGoogleClient, initializeOIDC };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etapes-7-8-routes-dauthentification-et-middleware\">\u00c9tapes 7-8 : Routes d&#8217;authentification et middleware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 7 : Cr\u00e9er les routes OAuth2 avec PKCE complet<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le fichier de routes g\u00e8re trois endpoints : le d\u00e9marrage du flux (<code>\/auth\/google<\/code>), la r\u00e9ception du callback (<code>\/auth\/google\/callback<\/code>) et la d\u00e9connexion (<code>\/auth\/logout<\/code>). Chaque \u00e9tape du flux inclut une validation stricte pour pr\u00e9venir les attaques par manipulation de param\u00e8tres.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ src\/routes\/auth.js\nconst express = require('express');\nconst { generators } = require('openid-client');\nconst { getGoogleClient } = require('..\/config\/oidc');\n\nconst router = express.Router();\n\n\/\/ --- GOOGLE OAUTH2 ---\n\n\/\/ \u00c9tape 7a : D\u00e9marrer le flux Authorization Code + PKCE\nrouter.get('\/google', async (req, res) => {\n  try {\n    const client = await getGoogleClient();\n\n    \/\/ G\u00e9n\u00e9rer les param\u00e8tres PKCE\n    const code_verifier = generators.codeVerifier(); \/\/ 128 chars, URL-safe base64\n    const code_challenge = generators.codeChallenge(code_verifier); \/\/ SHA-256 hash\n    const state = generators.state(); \/\/ Nonce al\u00e9atoire anti-CSRF\n\n    \/\/ Stocker en session (c\u00f4t\u00e9 serveur, jamais transmis au client)\n    req.session.code_verifier = code_verifier;\n    req.session.state = state;\n    req.session.returnTo = req.session.returnTo || '\/dashboard';\n\n    \/\/ Construire l'URL d'autorisation Google\n    const authUrl = client.authorizationUrl({\n      scope: 'openid email profile',\n      code_challenge,\n      code_challenge_method: 'S256',\n      state,\n      access_type: 'offline', \/\/ Demander un refresh_token\n      prompt: 'consent',      \/\/ Forcer l'affichage du consentement (pour refresh_token)\n    });\n\n    res.redirect(authUrl);\n  } catch (err) {\n    console.error('Erreur d\u00e9marrage OAuth2 Google:', err.message);\n    res.redirect('\/?error=oauth_init_failed');\n  }\n});\n\n\/\/ \u00c9tape 7b : Traiter le callback Google\nrouter.get('\/google\/callback', async (req, res) => {\n  try {\n    const client = await getGoogleClient();\n\n    \/\/ R\u00e9cup\u00e9rer et supprimer les valeurs PKCE de la session\n    const code_verifier = req.session.code_verifier;\n    const state = req.session.state;\n    const returnTo = req.session.returnTo || '\/dashboard';\n\n    delete req.session.code_verifier;\n    delete req.session.state;\n\n    if (!code_verifier || !state) {\n      return res.redirect('\/?error=session_expired');\n    }\n\n    \/\/ Valider le callback et \u00e9changer le code d'autorisation contre des tokens\n    const params = client.callbackParams(req);\n    const tokenSet = await client.callback(\n      process.env.GOOGLE_REDIRECT_URI,\n      params,\n      {\n        code_verifier, \/\/ Validation PKCE\n        state,         \/\/ Validation anti-CSRF\n      }\n    );\n\n    \/\/ R\u00e9cup\u00e9rer les informations utilisateur depuis le userinfo endpoint OIDC\n    const userInfo = await client.userinfo(tokenSet.access_token);\n\n    \/\/ R\u00e9g\u00e9n\u00e9rer la session pour pr\u00e9venir la fixation de session\n    req.session.regenerate((err) => {\n      if (err) return res.redirect('\/?error=session_error');\n\n      req.session.user = {\n        sub: userInfo.sub,\n        email: userInfo.email,\n        name: userInfo.name,\n        picture: userInfo.picture,\n        provider: 'google',\n      };\n\n      req.session.tokens = {\n        access_token: tokenSet.access_token,\n        refresh_token: tokenSet.refresh_token,\n        expires_at: tokenSet.expires_at,\n        id_token: tokenSet.id_token,\n      };\n\n      res.redirect(returnTo);\n    });\n\n  } catch (err) {\n    console.error('Erreur callback OAuth2 Google:', err.message);\n    res.redirect('\/?error=auth_failed');\n  }\n});\n\n\/\/ \u00c9tape 7c : D\u00e9connexion compl\u00e8te (locale + Google)\nrouter.get('\/logout', async (req, res) => {\n  const id_token = req.session.tokens?.id_token;\n\n  req.session.destroy((err) => {\n    if (err) console.error('Erreur destruction session:', err);\n  });\n\n  try {\n    if (id_token) {\n      const client = await getGoogleClient();\n      const endSessionUrl = client.endSessionUrl({\n        id_token_hint: id_token,\n        post_logout_redirect_uri: process.env.APP_BASE_URL,\n      });\n      return res.redirect(endSessionUrl);\n    }\n  } catch (err) {\n    \/\/ En cas d'erreur OIDC, d\u00e9connecter localement quand m\u00eame\n  }\n\n  res.redirect('\/');\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 8 : Cr\u00e9er le middleware d&#8217;authentification<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le middleware <code>requireAuth<\/code> v\u00e9rifie la pr\u00e9sence d&#8217;un utilisateur en session et sauvegarde l&#8217;URL demand\u00e9e pour une redirection post-login. Le middleware <code>checkTokenExpiry<\/code> renouvelle automatiquement les access tokens qui arrivent \u00e0 expiration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ src\/middleware\/auth.js\nconst { getGoogleClient } = require('..\/config\/oidc');\n\nfunction requireAuth(req, res, next) {\n  if (!req.session?.user) {\n    req.session.returnTo = req.originalUrl;\n    return res.redirect('\/auth\/google');\n  }\n  next();\n}\n\nasync function checkTokenExpiry(req, res, next) {\n  if (!req.session?.tokens) return next();\n\n  const { expires_at, refresh_token, access_token } = req.session.tokens;\n  const now = Math.floor(Date.now() \/ 1000);\n\n  \/\/ Renouveler si le token expire dans moins de 5 minutes\n  const shouldRefresh = expires_at\n    ? (expires_at - now < 300)\n    : false;\n\n  if (shouldRefresh &#038;&#038; refresh_token &#038;&#038; req.session.user?.provider === 'google') {\n    try {\n      const client = await getGoogleClient();\n      const newTokenSet = await client.refresh(refresh_token);\n\n      req.session.tokens = {\n        access_token: newTokenSet.access_token,\n        \/\/ Certains fournisseurs pratiquent la rotation : toujours conserver le nouveau\n        refresh_token: newTokenSet.refresh_token || refresh_token,\n        expires_at: newTokenSet.expires_at,\n        id_token: newTokenSet.id_token || req.session.tokens.id_token,\n      };\n    } catch (err) {\n      console.error('\u00c9chec renouvellement token:', err.message);\n      req.session.destroy();\n      return res.redirect('\/auth\/google');\n    }\n  }\n\n  next();\n}\n\nmodule.exports = { requireAuth, checkTokenExpiry };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-9-flux-pkce-pas-a-pas\">\u00c9tape 9 : Flux PKCE pas \u00e0 pas<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voici le d\u00e9roulement complet du flux Authorization Code + PKCE tel qu'il s'ex\u00e9cute dans le code ci-dessus :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>G\u00e9n\u00e9ration du code_verifier<\/strong> : l'application g\u00e9n\u00e8re une cha\u00eene al\u00e9atoire cryptographiquement s\u00fbre de 128 caract\u00e8res via <code>generators.codeVerifier()<\/code>. Cette valeur reste secr\u00e8te, stock\u00e9e en session c\u00f4t\u00e9 serveur.<\/li>\n\n\n\n<li><strong>Calcul du code_challenge<\/strong> : l'application calcule <code>BASE64URL(SHA-256(code_verifier))<\/code> via <code>generators.codeChallenge(code_verifier)<\/code>. Ce hash est public et envoy\u00e9 au serveur d'autorisation.<\/li>\n\n\n\n<li><strong>Redirection vers Google<\/strong> : l'utilisateur est envoy\u00e9 vers <code>accounts.google.com\/o\/oauth2\/auth<\/code> avec le <code>code_challenge<\/code>, la m\u00e9thode <code>S256<\/code>, le <code>state<\/code> anti-CSRF, et les scopes demand\u00e9s.<\/li>\n\n\n\n<li><strong>Authentification et consentement<\/strong> : l'utilisateur se connecte \u00e0 Google et accorde les permissions. Google stocke le <code>code_challenge<\/code> associ\u00e9 \u00e0 cette session.<\/li>\n\n\n\n<li><strong>Code d'autorisation renvoy\u00e9<\/strong> : Google redirige vers <code>\/auth\/google\/callback<\/code> avec un code d'autorisation \u00e0 usage unique, valable 10 minutes maximum.<\/li>\n\n\n\n<li><strong>\u00c9change token avec PKCE<\/strong> : l'application envoie le code d'autorisation ET le <code>code_verifier<\/code> original \u00e0 l'endpoint token de Google.<\/li>\n\n\n\n<li><strong>V\u00e9rification et \u00e9mission des tokens<\/strong> : Google recalcule SHA-256(code_verifier) et compare avec le <code>code_challenge<\/code> stock\u00e9. En cas de correspondance, Google \u00e9met l'access_token (validit\u00e9 1 heure), le refresh_token et l'id_token.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Un attaquant qui intercepte le code d'autorisation \u00e0 l'\u00e9tape 5 (via un malware, une mauvaise configuration HTTPS, ou un log de serveur) est bloqu\u00e9 \u00e0 l'\u00e9tape 6 : sans le <code>code_verifier<\/code>, le code intercept\u00e9 est inutilisable. Pour approfondir la gestion s\u00e9curis\u00e9e des cl\u00e9s et certificats utilis\u00e9s dans les protocoles TLS sous-jacents, consultez l'article <a href=\"\/fr\/openssl-cles-certificats-tutoriel\/\">OpenSSL : cl\u00e9s et certificats en 12 \u00e9tapes<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etapes-10-11-refresh-tokens-et-integration-github\">\u00c9tapes 10-11 : Refresh tokens et int\u00e9gration GitHub<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 10 : Service de gestion des refresh tokens<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le middleware <code>checkTokenExpiry<\/code> g\u00e8re le renouvellement automatique. Pour les cas plus complexes (renouvellement forc\u00e9, v\u00e9rification de validit\u00e9 sans requ\u00eate HTTP), cr\u00e9ez un service d\u00e9di\u00e9 :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ src\/services\/tokenService.js\nconst { getGoogleClient } = require('..\/config\/oidc');\n\nfunction isTokenExpired(tokens) {\n  if (!tokens?.expires_at) return false; \/\/ Sans expiration connue, supposer valide\n  const now = Math.floor(Date.now() \/ 1000);\n  return tokens.expires_at <= now + 60; \/\/ Marge de 60 secondes\n}\n\nasync function refreshGoogleToken(session) {\n  if (!session.tokens?.refresh_token) {\n    throw new Error('Aucun refresh_token disponible. Reconnexion requise.');\n  }\n\n  const client = await getGoogleClient();\n\n  const newTokenSet = await client.refresh(session.tokens.refresh_token);\n\n  \/\/ IMPORTANT : Google pratique la rotation des refresh_tokens sur certains \u00e9v\u00e9nements\n  \/\/ (changement de mot de passe, r\u00e9vocation). Toujours conserver le nouveau refresh_token.\n  session.tokens = {\n    access_token: newTokenSet.access_token,\n    refresh_token: newTokenSet.refresh_token || session.tokens.refresh_token,\n    expires_at: newTokenSet.expires_at,\n    id_token: newTokenSet.id_token || session.tokens.id_token,\n  };\n\n  return session.tokens;\n}\n\nmodule.exports = { isTokenExpired, refreshGoogleToken };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c9tape 11 : Int\u00e9gration GitHub OAuth2<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub utilise OAuth2 standard sans couche OIDC. Diff\u00e9rences cl\u00e9s : pas de d\u00e9couverte automatique, pas d'id_token JWT, et les informations utilisateur se r\u00e9cup\u00e8rent via l'API REST <code>api.github.com\/user<\/code>. L'utilisation de <code>fetch<\/code> natif (Node.js 18+) \u00e9vite une d\u00e9pendance externe.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ Ajout dans src\/routes\/auth.js\n\nconst GITHUB_AUTH_URL = 'https:\/\/github.com\/login\/oauth\/authorize';\nconst GITHUB_TOKEN_URL = 'https:\/\/github.com\/login\/oauth\/access_token';\n\n\/\/ D\u00e9marrer le flux GitHub\nrouter.get('\/github', (req, res) => {\n  const state = generators.state();\n  req.session.github_state = state;\n\n  const params = new URLSearchParams({\n    client_id: process.env.GITHUB_CLIENT_ID,\n    redirect_uri: process.env.GITHUB_REDIRECT_URI,\n    scope: 'read:user user:email',\n    state,\n  });\n\n  res.redirect(`${GITHUB_AUTH_URL}?${params}`);\n});\n\n\/\/ Traiter le callback GitHub\nrouter.get('\/github\/callback', async (req, res) => {\n  const { code, state, error } = req.query;\n\n  if (error) return res.redirect(`\/?error=${encodeURIComponent(error)}`);\n\n  \/\/ Validation du state anti-CSRF\n  if (!state || state !== req.session.github_state) {\n    return res.redirect('\/?error=state_mismatch');\n  }\n  delete req.session.github_state;\n\n  try {\n    \/\/ \u00c9changer le code contre un access_token GitHub\n    const tokenResponse = await fetch(GITHUB_TOKEN_URL, {\n      method: 'POST',\n      headers: {\n        'Accept': 'application\/json',\n        'Content-Type': 'application\/json',\n      },\n      body: JSON.stringify({\n        client_id: process.env.GITHUB_CLIENT_ID,\n        client_secret: process.env.GITHUB_CLIENT_SECRET,\n        code,\n        redirect_uri: process.env.GITHUB_REDIRECT_URI,\n      }),\n    });\n\n    const tokenData = await tokenResponse.json();\n    if (tokenData.error) {\n      throw new Error(`GitHub OAuth error: ${tokenData.error_description}`);\n    }\n\n    \/\/ R\u00e9cup\u00e9rer le profil utilisateur via l'API GitHub\n    const userResponse = await fetch('https:\/\/api.github.com\/user', {\n      headers: {\n        'Authorization': `Bearer ${tokenData.access_token}`,\n        'Accept': 'application\/vnd.github.v3+json',\n        'User-Agent': 'OAuth2-Node-App',\n      },\n    });\n\n    if (!userResponse.ok) {\n      throw new Error(`GitHub API error: ${userResponse.status}`);\n    }\n\n    const githubUser = await userResponse.json();\n\n    req.session.regenerate((err) => {\n      if (err) return res.redirect('\/?error=session_error');\n\n      req.session.user = {\n        sub: String(githubUser.id),\n        email: githubUser.email,\n        name: githubUser.name || githubUser.login,\n        picture: githubUser.avatar_url,\n        provider: 'github',\n      };\n      req.session.tokens = {\n        access_token: tokenData.access_token,\n        expires_at: null, \/\/ GitHub ne fournit pas d'expiration par d\u00e9faut\n      };\n\n      res.redirect('\/dashboard');\n    });\n\n  } catch (err) {\n    console.error('Erreur GitHub OAuth:', err.message);\n    res.redirect('\/?error=github_auth_failed');\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-12-tester-et-valider-la-securite\">\u00c9tape 12 : Tester et valider la s\u00e9curit\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">D\u00e9marrez l'application avec <code>npm run dev<\/code> et testez le flux complet. La console affiche les messages de d\u00e9couverte OIDC et les logs d'authentification. Ouvrez <code>http:\/\/localhost:3000<\/code>, cliquez sur \"Connexion avec Google\" et suivez le flux :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>La page Google s'affiche avec la demande de permissions \"email\" et \"profil\"<\/li>\n\n\n\n<li>Apr\u00e8s approbation, vous \u00eates redirig\u00e9 vers <code>\/dashboard<\/code> avec vos informations utilisateur<\/li>\n\n\n\n<li>Ouvrez les DevTools navigateur (Onglet Application &gt; Cookies) et v\u00e9rifiez que le cookie <code>sid<\/code> est marqu\u00e9 <code>HttpOnly<\/code><\/li>\n\n\n\n<li>Testez la d\u00e9connexion et v\u00e9rifiez que <code>\/dashboard<\/code> redirige vers Google<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Voici la liste de contr\u00f4le de s\u00e9curit\u00e9 \u00e0 valider avant tout d\u00e9ploiement en production :<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Point de contr\u00f4le<\/th><th>Priorit\u00e9<\/th><th>Action corrective<\/th><\/tr><\/thead><tbody><tr><td>Cookie HttpOnly activ\u00e9<\/td><td>Critique<\/td><td>V\u00e9rifier <code>httpOnly: true<\/code> dans la config session<\/td><\/tr><tr><td>Cookie Secure en production<\/td><td>Critique<\/td><td>D\u00e9ployer derri\u00e8re HTTPS, activer <code>secure: true<\/code><\/td><\/tr><tr><td>PKCE m\u00e9thode S256<\/td><td>Critique<\/td><td>V\u00e9rifier <code>code_challenge_method: 'S256'<\/code><\/td><\/tr><tr><td>Param\u00e8tre state valid\u00e9<\/td><td>Critique<\/td><td>Comparer state re\u00e7u avec state en session<\/td><\/tr><tr><td>Session r\u00e9g\u00e9n\u00e9r\u00e9e apr\u00e8s login<\/td><td>\u00c9lev\u00e9e<\/td><td>Appeler <code>req.session.regenerate()<\/code> apr\u00e8s auth<\/td><\/tr><tr><td>Secrets hors du code source<\/td><td>Critique<\/td><td>Variables d'environnement + .gitignore<\/td><\/tr><tr><td>En-t\u00eates Helmet configur\u00e9s<\/td><td>\u00c9lev\u00e9e<\/td><td>CSP, HSTS, X-Frame-Options activ\u00e9s<\/td><\/tr><tr><td>Store de session Redis<\/td><td>Critique (production)<\/td><td>Remplacer MemoryStore par connect-redis<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pour configurer Redis comme store de sessions persistant, l'article <a href=\"\/fr\/nodejs-session-management\/\">Node.js Session Management<\/a> couvre l'int\u00e9gration compl\u00e8te avec connect-redis, la rotation des secrets et les strat\u00e9gies d'expiration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"les-7-pieges-courants-avec-oauth2-en-node-js\">Les 7 pi\u00e8ges courants avec OAuth2 en Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 1 : Stocker les tokens dans localStorage<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">C'est l'erreur la plus r\u00e9pandue dans les tutoriels en ligne. <code>localStorage<\/code> est accessible par tout JavaScript ex\u00e9cut\u00e9 sur la page, ce qui rend les tokens vuln\u00e9rables aux attaques XSS. Un seul script malveillant inject\u00e9 (via une d\u00e9pendance npm compromise ou une injection XSS) peut exfiltrer tous les tokens en quelques millisecondes. Stockez toujours les tokens dans des cookies <code>httpOnly<\/code> ou en session c\u00f4t\u00e9 serveur. Les SPAs qui consomment une API peuvent utiliser des cookies SameSite=Strict avec un proxy backend.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 2 : Omettre PKCE parce que l'application a un client_secret<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Beaucoup d'impl\u00e9mentations legacy utilisent le flux Authorization Code sans PKCE au motif que leur application c\u00f4t\u00e9 serveur poss\u00e8de un <code>client_secret<\/code>. C'est insuffisant : si le callback URL est compromis ou si un malware surveille les redirections du navigateur, le code d'autorisation peut \u00eatre intercept\u00e9 et \u00e9chang\u00e9 depuis un autre serveur. PKCE est obligatoire dans OAuth 2.1 et doit \u00eatre trait\u00e9 comme tel, m\u00eame pour les clients confidentiels.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 3 : Ne pas valider le param\u00e8tre state<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Omettre la validation du <code>state<\/code> expose l'application aux attaques Login CSRF : un attaquant initie un flux OAuth2 et manipule la victime pour terminer l'authentification avec le compte de l'attaquant (ce qui lie le compte de la victime aux credentials de l'attaquant). G\u00e9n\u00e9rez toujours un <code>state<\/code> cryptographiquement al\u00e9atoire, stockez-le en session avant la redirection, et comparez-le strictement \u00e0 la valeur re\u00e7ue dans le callback.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 4 : Tokens d'acc\u00e8s de longue dur\u00e9e<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Configurer des access tokens valables 24 heures annule l'un des principaux avantages d'OAuth2. Les bonnes pratiques 2025-2026 recommandent des access tokens valables 15 \u00e0 60 minutes, avec des refresh tokens pour le renouvellement silencieux. Un token vol\u00e9 devient inutile tr\u00e8s rapidement, limitant les dommages en cas de compromission. Google \u00e9met des access tokens d'exactement 3 600 secondes (1 heure) par d\u00e9faut.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 5 : Session en m\u00e9moire en production<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le store de session par d\u00e9faut d'express-session est <strong>MemoryStore<\/strong>, explicitement document\u00e9 comme non adapt\u00e9 \u00e0 la production. Il fuit de la m\u00e9moire avec le nombre de connexions simultan\u00e9es, perd toutes les sessions lors d'un red\u00e9marrage serveur, et emp\u00eache le scaling horizontal (plusieurs instances Node.js). En production, utilisez Redis avec <code>connect-redis<\/code> pour des sessions persistantes, partag\u00e9es et scalables.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 6 : redirect_uri valid\u00e9e par pr\u00e9fixe plut\u00f4t que par correspondance exacte<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Certains d\u00e9veloppeurs enregistrent <code>https:\/\/monapp.com\/<\/code> et acceptent n'importe quelle URL d\u00e9butant par ce pr\u00e9fixe. Le serveur d'autorisation doit comparer l'URI de redirection par correspondance exacte de cha\u00eene. Si votre fournisseur tol\u00e8re une validation laxiste, un attaquant peut rediriger le code d'autorisation vers <code>https:\/\/monapp.com.evil.com\/<\/code> ou exploiter des open redirectors sur votre domaine. Enregistrez toujours les URIs compl\u00e8tes et pr\u00e9cises.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pi\u00e8ge 7 : Ne pas r\u00e9voquer la session c\u00f4t\u00e9 fournisseur \u00e0 la d\u00e9connexion<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">D\u00e9truire uniquement la session Express locale laisse la session active c\u00f4t\u00e9 fournisseur d'identit\u00e9. Si l'utilisateur est sur un ordinateur partag\u00e9, une autre personne peut rouvrir le navigateur et se connecter directement via le SSO Google, sans passer par votre application. Utilisez toujours l'<code>end session endpoint<\/code> OIDC (couvert \u00e0 l'\u00e9tape 7c) pour invalider la session c\u00f4t\u00e9 fournisseur. Pour les fournisseurs sans OIDC, appelez l'endpoint de r\u00e9vocation OAuth2 (RFC 7009) si disponible.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"depannage-8-erreurs-oauth2-frequentes\">D\u00e9pannage : 8 erreurs OAuth2 fr\u00e9quentes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 1 : <code>redirect_uri_mismatch<\/code><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L'URI de redirection dans la requ\u00eate ne correspond pas exactement \u00e0 celle enregistr\u00e9e. V\u00e9rifiez : pr\u00e9sence ou absence du slash final (<code>\/callback<\/code> vs <code>\/callback\/<\/code>), protocole (<code>http<\/code> vs <code>https<\/code>), num\u00e9ro de port (<code>:3000<\/code>), et casse exacte. Copiez-collez l'URI directement depuis la console du fournisseur vers votre <code>.env<\/code> pour \u00e9viter toute erreur.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 2 : <code>invalid_grant<\/code><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le code d'autorisation a d\u00e9j\u00e0 \u00e9t\u00e9 utilis\u00e9 (il est \u00e0 usage unique), a expir\u00e9 (10 minutes maximum chez Google), ou le <code>code_verifier<\/code> ne correspond pas au <code>code_challenge<\/code> envoy\u00e9. Ne tentez jamais de rejouer un code. Chaque clic sur \"Connexion avec Google\" g\u00e9n\u00e8re un nouveau couple code_verifier\/code_challenge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 3 : <code>invalid_client<\/code><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le <code>client_id<\/code> ou le <code>client_secret<\/code> est incorrect. V\u00e9rifiez que les variables d'environnement sont bien charg\u00e9es en ajoutant temporairement <code>console.log('CLIENT_ID:', process.env.GOOGLE_CLIENT_ID?.substring(0, 10))<\/code> au d\u00e9marrage. Assurez-vous que le fichier <code>.env<\/code> se trouve \u00e0 la racine du projet et que <code>require('dotenv').config()<\/code> est la premi\u00e8re instruction du fichier principal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 4 : Session perdue entre les requ\u00eates (code_verifier manquant)<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le <code>code_verifier<\/code> stock\u00e9 en session lors de <code>\/auth\/google<\/code> est absent lors du callback. Causes fr\u00e9quentes : cookie de session non transmis (v\u00e9rifier <code>SameSite=Lax<\/code> et domaine correct), deux instances de l'application avec des secrets de session diff\u00e9rents, ou rechargement de page pendant le flux. En d\u00e9veloppement, utilisez exclusivement <code>localhost<\/code> (pas <code>127.0.0.1<\/code>) pour la coh\u00e9rence des cookies.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 5 : <code>checks.state argument is missing<\/code><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">openid-client exige que le <code>state<\/code> soit pass\u00e9 \u00e0 <code>client.callback()<\/code> pour validation. V\u00e9rifiez que <code>req.session.state<\/code> est d\u00e9fini avant la redirection et qu'il est inclus dans l'objet de v\u00e9rification. Cette erreur appara\u00eet fr\u00e9quemment quand la session expire entre la requ\u00eate initiale et le callback (plus de quelques minutes).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 6 : Token expir\u00e9 sur toutes les requ\u00eates prot\u00e9g\u00e9es<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le middleware <code>checkTokenExpiry<\/code> doit \u00eatre plac\u00e9 globalement avec <code>app.use(checkTokenExpiry)<\/code>, apr\u00e8s la configuration des sessions mais avant les routes prot\u00e9g\u00e9es. S'il est seulement appliqu\u00e9 sur certaines routes, d'autres routes continueront \u00e0 utiliser des tokens expir\u00e9s jusqu'\u00e0 ce qu'une erreur 401 d\u00e9clenche une reconnexion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 7 : Erreurs CORS sur l'API Node.js<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les requ\u00eates depuis un SPA frontend vers votre API Node.js d\u00e9clenchent des erreurs CORS si les en-t\u00eates <code>Access-Control-Allow-Origin<\/code> ne sont pas configur\u00e9s. Installez <code>cors<\/code> et configurez les origines autoris\u00e9es explicitement (\u00e9vitez <code>*<\/code> avec des credentials). Pour les routes prot\u00e9g\u00e9es par tokens Bearer OAuth2, exposez l'en-t\u00eate <code>Authorization<\/code> dans la configuration CORS : <code>allowedHeaders: ['Authorization', 'Content-Type']<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Erreur 8 : Timeout lors de la d\u00e9couverte OIDC au d\u00e9marrage<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Issuer.discover()<\/code> effectue une requ\u00eate r\u00e9seau au d\u00e9marrage. Dans les environnements Docker ou Kubernetes avec restrictions r\u00e9seau sortant, cette requ\u00eate peut \u00e9chouer ou timeout. Solution : encapsulez l'appel dans une boucle de retry avec backoff exponentiel (3 tentatives, d\u00e9lai doubl\u00e9 \u00e0 chaque fois), et retardez l'\u00e9coute du port HTTP avec <code>app.listen()<\/code> jusqu'\u00e0 la r\u00e9ussite de la d\u00e9couverte OIDC. Ajoutez \u00e9galement un timeout de 10 secondes sur chaque tentative.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour renforcer la protection de vos endpoints d'authentification contre les attaques de force brute et le credential stuffing, l'article <a href=\"\/fr\/hmac-sha256-nodejs\/\">HMAC-SHA256 en Node.js<\/a> couvre la signature des webhooks et la d\u00e9tection des requ\u00eates forg\u00e9es.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conseils-avances\">Conseils avanc\u00e9s<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"support-multi-fournisseurs-avec-passport-js\">Support multi-fournisseurs avec Passport.js<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pour une application g\u00e9rant plusieurs fournisseurs OAuth2 (Google, GitHub, Microsoft, LinkedIn), Passport.js unifie l'orchestration. Chaque fournisseur s'enregistre via <code>passport.use()<\/code> avec une strat\u00e9gie d\u00e9di\u00e9e. La s\u00e9rialisation stocke uniquement le couple <code>(provider, user.id)<\/code> en session, sans tokens, ce qui minimise la taille des cookies et am\u00e9liore la s\u00e9curit\u00e9. <code>passport.deserializeUser<\/code> recharge l'utilisateur depuis la base de donn\u00e9es \u00e0 chaque requ\u00eate authentifi\u00e9e. Combinez Passport avec openid-client pour garder la conformit\u00e9 PKCE : les strat\u00e9gies Passport officielles pour OIDC (comme <code>passport-openidconnect<\/code>) s'appuient sur openid-client en interne depuis 2024.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"resource-server-valider-les-tokens-bearer-entrants\">Resource server : valider les tokens Bearer entrants<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Quand votre API Node.js joue le r\u00f4le de resource server (validation des tokens \u00e9mis par Auth0, Okta ou un serveur d'autorisation interne), utilisez <code>express-oauth2-jwt-bearer<\/code>. Ce middleware v\u00e9rifie la signature JWT via les JWKS publics du fournisseur, l'\u00e9metteur (<code>iss<\/code>), l'audience (<code>aud<\/code>), l'expiration (<code>exp<\/code>) et les scopes requis en une seule ligne. Retournez <code>401<\/code> pour les tokens manquants ou invalides, et <code>403<\/code> pour les tokens valides mais sans les scopes requis. Cette distinction est fondamentale : ne m\u00e9langez jamais ces deux codes d'erreur, sous peine de masquer des probl\u00e8mes d'autorisation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sessions-redis-en-production\">Sessions Redis en production<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Remplacez MemoryStore par Redis avec <code>connect-redis<\/code> en 4 lignes :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">const { createClient } = require('redis');\nconst RedisStore = require('connect-redis').default;\n\nconst redisClient = createClient({ url: process.env.REDIS_URL });\nawait redisClient.connect();\n\napp.use(session({\n  store: new RedisStore({ client: redisClient, prefix: 'oauth2sess:' }),\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: { secure: true, httpOnly: true, sameSite: 'lax', maxAge: 86400000 },\n}));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Redis permet \u00e9galement d'impl\u00e9menter une liste noire de sessions r\u00e9voqu\u00e9es (logout global de tous les appareils), de partager les sessions entre plusieurs instances Node.js derri\u00e8re un load balancer, et de configurer une expiration TTL automatique des sessions inactives. La cl\u00e9 prefix <code>oauth2sess:<\/code> facilite l'administration et la surveillance des sessions dans l'interface Redis.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq-sur-oauth2-en-node-js\">FAQ sur OAuth2 en Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Quelle est la diff\u00e9rence entre OAuth2 et OpenID Connect ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2 est un protocole d'<em>autorisation<\/em> : il permet \u00e0 une application d'acc\u00e9der \u00e0 des ressources au nom d'un utilisateur. OpenID Connect (OIDC) est une couche d'<em>authentification<\/em> construite au-dessus d'OAuth2 : il ajoute un <code>id_token<\/code> JWT qui contient des informations d'identit\u00e9 v\u00e9rifiables (sub, email, name). Utilisez OIDC quand vous avez besoin de savoir <em>qui<\/em> est l'utilisateur. Utilisez OAuth2 seul quand vous avez besoin d'acc\u00e9der \u00e0 une API tierce au nom de l'utilisateur (exemple : lire ses emails Gmail).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Passport.js ou openid-client directement ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour une application avec un seul fournisseur OIDC, openid-client directement offre plus de contr\u00f4le sur le protocole et une impl\u00e9mentation plus l\u00e9g\u00e8re (aucune couche d'abstraction). Pour une application avec plusieurs strat\u00e9gies d'authentification (local + Google + GitHub + SAML), Passport.js unifie la gestion des sessions et des strat\u00e9gies. Les deux sont compl\u00e9mentaires : openid-client pour le protocole, Passport pour l'orchestration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pourquoi le flux Implicit est-il d\u00e9pr\u00e9ci\u00e9 ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le flux Implicit \u00e9mettait les tokens directement dans le fragment URL de la redirection (<code>#access_token=...&token_type=bearer<\/code>), les exposant \u00e0 l'historique du navigateur, aux logs serveur et aux en-t\u00eates Referer. OAuth 2.1 supprime ce flux au profit du flux Authorization Code + PKCE, qui offre les m\u00eames avantages pour les SPAs sans exposition des tokens dans l'URL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>OAuth2 est-il conforme RGPD ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2 est un protocole technique, pas une garantie de conformit\u00e9 RGPD par lui-m\u00eame. Cependant, il facilite la conformit\u00e9 par ses scopes granulaires (principe du moindre privil\u00e8ge), la tra\u00e7abilit\u00e9 des acc\u00e8s via les logs du serveur d'autorisation, et la r\u00e9vocation imm\u00e9diate des acc\u00e8s. Documentez les scopes demand\u00e9s et leur finalit\u00e9 dans votre politique de confidentialit\u00e9, conform\u00e9ment \u00e0 l'article 13 du RGPD.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Comment g\u00e9rer plusieurs sessions simultan\u00e9es par utilisateur ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Associez chaque session \u00e0 un <code>device_id<\/code> stock\u00e9 dans un cookie distinct non-httpOnly. C\u00f4t\u00e9 serveur, maintenez une table Redis des sessions actives par <code>user_id<\/code> (cl\u00e9 : <code>user:sessions:{user_id}<\/code>, valeur : liste des session IDs actifs). Cette architecture permet la r\u00e9vocation de sessions individuelles (d\u00e9connexion d'un seul appareil) ou la d\u00e9connexion globale, une fonctionnalit\u00e9 attendue dans les applications professionnelles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Comment tester OAuth2 sans fournisseur externe ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Utilisez <strong>node-oidc-provider<\/strong> (npm), qui d\u00e9ploie un serveur OIDC complet en une vingtaine de lignes Node.js. Ou <strong>Ory Hydra<\/strong> via Docker pour une impl\u00e9mentation OAuth2\/OIDC en production. Ces solutions permettent d'\u00e9crire des tests d'int\u00e9gration couvrant le flux complet sans d\u00e9pendre de fournisseurs externes, \u00e9liminant les tests flaky dus aux timeouts r\u00e9seau et aux limites de rate limiting des APIs Google ou GitHub.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Comment s\u00e9curiser les refresh tokens ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les refresh tokens ont une dur\u00e9e de vie longue (jours \u00e0 mois) et permettent d'obtenir de nouveaux access tokens sans interaction utilisateur. Stockez-les exclusivement c\u00f4t\u00e9 serveur (session Redis ou base de donn\u00e9es chiffr\u00e9e), jamais dans le navigateur. Impl\u00e9mentez la rotation des refresh tokens : \u00e0 chaque utilisation, le serveur d'autorisation \u00e9met un nouveau refresh token et invalide l'ancien. Google active la rotation automatiquement dans certaines circonstances (changement de mot de passe, r\u00e9vocation de l'application).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"liens-connexes\">Liens connexes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"articles-lies-sur-shattered-io\">Articles li\u00e9s sur shattered.io<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/fr\/jwt-authentication-nodejs\/\">JWT Authentication en Node.js : 10 \u00e9tapes<\/a> : comprendre les tokens JWT utilis\u00e9s dans OAuth2<\/li>\n\n\n\n<li><a href=\"\/fr\/authentification-totp-nodejs\/\">Authentification TOTP en Node.js : 2FA en 12 \u00e9tapes<\/a> : combiner OAuth2 avec la double authentification<\/li>\n\n\n\n<li><a href=\"\/fr\/csrf-protection-nodejs\/\">CSRF Protection en Node.js : 12 \u00e9tapes<\/a> : prot\u00e9ger les formulaires et les routes OAuth2<\/li>\n\n\n\n<li><a href=\"\/fr\/nodejs-session-management\/\">Node.js Session Management : 11 \u00e9tapes<\/a> : configurer Redis pour les sessions OAuth2<\/li>\n\n\n\n<li><a href=\"\/fr\/lets-encrypt-nginx-https-tutoriel\/\">Let's Encrypt + Nginx : HTTPS en 12 \u00e9tapes<\/a> : s\u00e9curiser les cookies OAuth2 avec HTTPS<\/li>\n\n\n\n<li><a href=\"\/fr\/hmac-sha256-nodejs\/\">HMAC-SHA256 en Node.js : 12 \u00e9tapes<\/a> : signer les webhooks et les requ\u00eates API<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ressources officielles : <a href=\"https:\/\/oauth.net\/2\/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">sp\u00e9cification OAuth2 (oauth.net)<\/a>, <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">RFC 6749 IETF<\/a>, <a href=\"https:\/\/openid.net\/specs\/openid-connect-core-1_0.html\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">sp\u00e9cification OpenID Connect Core<\/a>, <a href=\"https:\/\/www.passportjs.org\/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">documentation Passport.js<\/a>, <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">OWASP Top 10<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>OAuth2 s\u00e9curise aujourd&#8217;hui plus de 90 % des applications web modernes, des connexions Google aux API d&#8217;entreprise. Ce tutoriel vous guide pas \u00e0 pas pour impl\u00e9menter OAuth2 avec le flux\u2026<\/p>\n","protected":false},"author":3,"featured_media":211,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-210","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\/210","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\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/comments?post=210"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/210\/revisions"}],"predecessor-version":[{"id":212,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/210\/revisions\/212"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media\/211"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media?parent=210"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/categories?post=210"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/tags?post=210"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}