{"id":291,"date":"2026-06-20T08:00:00","date_gmt":"2026-06-20T08:00:00","guid":{"rendered":"https:\/\/shattered.io\/fr\/2026\/06\/20\/keycloak-nodejs-oauth2-openid-connect\/"},"modified":"2026-06-20T16:49:32","modified_gmt":"2026-06-20T16:49:32","slug":"keycloak-nodejs-oauth2-openid-connect","status":"publish","type":"post","link":"https:\/\/shattered.io\/fr\/2026\/06\/20\/keycloak-nodejs-oauth2-openid-connect\/","title":{"rendered":"Keycloak dans Node.js : OAuth 2.0 et OpenID Connect en 12 \u00c9tapes [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Keycloak s&#8217;impose en 2026 comme la solution open source de r\u00e9f\u00e9rence pour la gestion des identit\u00e9s et des acc\u00e8s. Avec <strong>523 125 t\u00e9l\u00e9chargements mensuels<\/strong> pour le package <code>keycloak-connect<\/code> et <strong>3,8 millions<\/strong> pour <code>keycloak-js<\/code>, l&#8217;int\u00e9gration de Keycloak dans les applications Node.js est devenue une comp\u00e9tence incontournable pour tout d\u00e9veloppeur soucieux de s\u00e9curit\u00e9. La version <strong>26.6.3<\/strong>, publi\u00e9e le 4 juin 2026, apporte le JWT Authorization Grant, la f\u00e9d\u00e9ration de clients et la prise en charge native de DPoP, renfor\u00e7ant un \u00e9cosyst\u00e8me b\u00e2ti sur OAuth 2.0 et OpenID Connect.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ce tutoriel vous guide pas \u00e0 pas, de l&#8217;installation de Keycloak via Docker jusqu&#8217;\u00e0 la protection de vos routes Express avec rotation des refresh tokens et validation par introspection. Les CVE <strong>CVE-2026-0707<\/strong> (contournement de contr\u00f4les de s\u00e9curit\u00e9) et <strong>CVE-2026-2575<\/strong> (d\u00e9ni de service SAML), corrig\u00e9es dans la version 26.5.4, rappellent que configurer correctement son fournisseur d&#8217;identit\u00e9 n&#8217;est pas une option. En suivant ce guide, vous obtenez une API Node.js s\u00e9curis\u00e9e, conforme aux normes PKCE et OIDC, pr\u00eate pour la production en environnement europ\u00e9en.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequis-versions-outils-et-connaissances-requises\">Pr\u00e9requis : versions, outils et connaissances requises<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de d\u00e9marrer, assurez-vous de disposer des \u00e9l\u00e9ments suivants sur votre machine de d\u00e9veloppement. Toutes les versions list\u00e9es sont celles test\u00e9es dans ce tutoriel.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Composant<\/th><th>Version requise<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>20.x LTS ou 22.x<\/td><td>Modules ESM support\u00e9s nativement<\/td><\/tr><tr><td>npm<\/td><td>10.x ou sup\u00e9rieur<\/td><td>Inclus avec Node.js 20+<\/td><\/tr><tr><td>Docker<\/td><td>25.x ou sup\u00e9rieur<\/td><td>Pour lancer Keycloak en local<\/td><\/tr><tr><td>keycloak-connect<\/td><td>26.1.1<\/td><td>Middleware officiel Keycloak pour Express<\/td><\/tr><tr><td>Express<\/td><td>4.19.x<\/td><td>Framework HTTP Node.js<\/td><\/tr><tr><td>Keycloak Server<\/td><td>26.6.3<\/td><td>Derni\u00e8re version stable (4 juin 2026)<\/td><\/tr><tr><td>curl<\/td><td>8.x<\/td><td>Pour tester les endpoints REST<\/td><\/tr><tr><td>openssl<\/td><td>3.x<\/td><td>G\u00e9n\u00e9ration des certificats TLS locaux<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Connaissances recommand\u00e9es : bases de JavaScript asynchrone (async\/await), notions de HTTP (codes de r\u00e9ponse, headers), et compr\u00e9hension minimale des tokens JWT. Aucune exp\u00e9rience pr\u00e9alable avec Keycloak n&#8217;est n\u00e9cessaire. Ce tutoriel part de z\u00e9ro et explique chaque d\u00e9cision de configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"architecture-oauth-2-0-et-openid-connect-avec-keycloak\">Architecture OAuth 2.0 et OpenID Connect avec Keycloak<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Comprendre l&#8217;architecture avant d&#8217;\u00e9crire du code \u00e9vite les erreurs de conception co\u00fbteuses. OAuth 2.0, d\u00e9fini dans la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6749<\/a>, est un framework d&#8217;autorisation permettant \u00e0 une application tierce d&#8217;acc\u00e9der \u00e0 des ressources au nom d&#8217;un utilisateur. OpenID Connect (OIDC) est une couche d&#8217;identit\u00e9 construite sur OAuth 2.0, permettant en plus l&#8217;authentification de l&#8217;utilisateur via un ID Token sign\u00e9. Ces deux standards sont support\u00e9s nativement par Keycloak, qui joue le r\u00f4le de fournisseur d&#8217;identit\u00e9 (IdP) centralis\u00e9.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"les-quatre-acteurs-du-flux-authorization-code\">Les quatre acteurs du flux Authorization Code<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dans un d\u00e9ploiement Keycloak + Node.js, quatre acteurs interagissent. <strong>L&#8217;utilisateur<\/strong> (Resource Owner) initie l&#8217;authentification depuis son navigateur. <strong>Le client<\/strong> (votre application Node.js\/Express) redirige l&#8217;utilisateur vers Keycloak et consomme les tokens. <strong>Le serveur d&#8217;autorisation<\/strong> (Keycloak 26.6.3) valide les identifiants, applique les politiques de s\u00e9curit\u00e9 et \u00e9met les tokens. <strong>Le serveur de ressources<\/strong> (votre API Express) v\u00e9rifie les access tokens Bearer sur chaque requ\u00eate prot\u00e9g\u00e9e.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le flux complet se d\u00e9roule en cinq \u00e9tapes. L&#8217;application redirige l&#8217;utilisateur vers l&#8217;endpoint <code>\/authorize<\/code> de Keycloak avec un code challenge PKCE. L&#8217;utilisateur s&#8217;authentifie sur la page Keycloak (avec mot de passe, WebAuthn, OTP selon la configuration). Keycloak retourne un code d&#8217;autorisation \u00e0 l&#8217;URL de redirection. L&#8217;application \u00e9change ce code contre un access token, un refresh token et un ID token via l&#8217;endpoint <code>\/token<\/code>. Chaque requ\u00eate \u00e0 l&#8217;API inclut l&#8217;access token dans le header <code>Authorization: Bearer<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pourquoi-pkce-est-obligatoire-en-2026\">Pourquoi PKCE est obligatoire en 2026<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE (Proof Key for Code Exchange), d\u00e9fini dans la <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 7636<\/a>, est d\u00e9sormais obligatoire pour tous les clients publics selon les recommandations OAuth 2.1 (qui consolide les meilleures pratiques de s\u00e9curit\u00e9 OAuth). L&#8217;attaque vis\u00e9e : un attaquant intercepte le code d&#8217;autorisation retourn\u00e9 par Keycloak (via les logs du serveur, un malware, ou une redirection compromise) et l&#8217;\u00e9change contre des tokens avant l&#8217;application l\u00e9gitime. PKCE emp\u00eache cela en liant cryptographiquement le code challenge (SHA-256 du code verifier, envoy\u00e9 lors de la demande d&#8217;autorisation) au code verifier (envoy\u00e9 lors de l&#8217;\u00e9change). Sans la cl\u00e9 priv\u00e9e du verifier, le code intercept\u00e9 est inutilisable. Keycloak 26.6.3 force PKCE par d\u00e9faut pour tous les clients de type <em>public<\/em>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-1-installer-keycloak-26-6-3-via-docker\">\u00c9tape 1 : Installer Keycloak 26.6.3 via Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker est la m\u00e9thode la plus rapide pour d\u00e9marrer Keycloak en d\u00e9veloppement. L&#8217;image officielle disponible sur <code>quay.io\/keycloak\/keycloak<\/code> int\u00e8gre une base de donn\u00e9es H2 en m\u00e9moire adapt\u00e9e aux tests locaux. Pour la production, remplacez syst\u00e9matiquement H2 par PostgreSQL 16 ou sup\u00e9rieur.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Lancer Keycloak 26.6.3 en mode d\u00e9veloppement\ndocker run -d \\\n  --name keycloak-dev \\\n  -p 8080:8080 \\\n  -e KEYCLOAK_ADMIN=admin \\\n  -e KEYCLOAK_ADMIN_PASSWORD=admin_secret_2026 \\\n  quay.io\/keycloak\/keycloak:26.6.3 \\\n  start-dev\n\n# V\u00e9rifier que le conteneur est d\u00e9marr\u00e9 et attendre la disponibilit\u00e9\ndocker logs -f keycloak-dev 2>&1 | grep -m1 \"Keycloak.*started\"\n\n# Tester la disponibilit\u00e9 de l'endpoint de sant\u00e9\ncurl -s http:\/\/localhost:8080\/health\/ready | python3 -m json.tool<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La console d&#8217;administration est accessible sur <code>http:\/\/localhost:8080\/admin<\/code>. Le d\u00e9marrage complet prend entre 15 et 40 secondes selon votre machine. Le flag <code>start-dev<\/code> d\u00e9sactive les optimisations de production : TLS obligatoire, base H2 en m\u00e9moire (donn\u00e9es perdues au red\u00e9marrage), et endpoints de diagnostic activ\u00e9s. Ne l&#8217;utilisez jamais en production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour un environnement avec persistance des donn\u00e9es et PostgreSQL, utilisez Docker Compose :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># docker-compose.yml pour Keycloak 26.6.3 + PostgreSQL 16\nversion: '3.9'\nservices:\n  postgres:\n    image: postgres:16-alpine\n    environment:\n      POSTGRES_DB: keycloak\n      POSTGRES_USER: keycloak\n      POSTGRES_PASSWORD: kc_db_password_2026\n    volumes:\n      - postgres_data:\/var\/lib\/postgresql\/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keycloak\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    networks:\n      - keycloak-net\n\n  keycloak:\n    image: quay.io\/keycloak\/keycloak:26.6.3\n    command: start-dev\n    environment:\n      KC_DB: postgres\n      KC_DB_URL: jdbc:postgresql:\/\/postgres:5432\/keycloak\n      KC_DB_USERNAME: keycloak\n      KC_DB_PASSWORD: kc_db_password_2026\n      KEYCLOAK_ADMIN: admin\n      KEYCLOAK_ADMIN_PASSWORD: admin_secret_2026\n      KC_HOSTNAME: localhost\n      KC_METRICS_ENABLED: \"true\"\n      KC_LOG_LEVEL: INFO\n      KC_LOG_FORMAT: json\n    ports:\n      - \"8080:8080\"\n      - \"9000:9000\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n    networks:\n      - keycloak-net\n\nvolumes:\n  postgres_data:\n\nnetworks:\n  keycloak-net:<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le port 9000 expose les m\u00e9triques Prometheus et l&#8217;endpoint de sant\u00e9 (<code>\/health\/ready<\/code>, <code>\/metrics<\/code>). L&#8217;activation des m\u00e9triques (<code>KC_METRICS_ENABLED=true<\/code>) et des logs structur\u00e9s JSON (<code>KC_LOG_FORMAT=json<\/code>) sont des pr\u00e9requis pour int\u00e9grer Keycloak dans un stack d&#8217;observabilit\u00e9 (Grafana, ELK, Loki).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-2-creer-le-realm-et-le-client-dans-keycloak\">\u00c9tape 2 : Cr\u00e9er le Realm et le Client dans Keycloak<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un <strong>Realm<\/strong> dans Keycloak est un espace d&#8217;isolation complet qui regroupe utilisateurs, clients, r\u00f4les, f\u00e9d\u00e9rations d&#8217;identit\u00e9 et politiques de s\u00e9curit\u00e9. L&#8217;interface d&#8217;administration permet de tout configurer via l&#8217;UI, mais l&#8217;API REST de Keycloak offre une approche reproductible et automatisable en CI\/CD. Cr\u00e9er votre realm via l&#8217;API garantit l&#8217;idempotence des d\u00e9ploiements.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"creer-le-realm-et-le-client-via-lapi-rest\">Cr\u00e9er le Realm et le Client via l&#8217;API REST<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># 1. Obtenir un token admin depuis le realm master\nTOKEN=$(curl -s -X POST http:\/\/localhost:8080\/realms\/master\/protocol\/openid-connect\/token \\\n  -H 'Content-Type: application\/x-www-form-urlencoded' \\\n  -d 'client_id=admin-cli' \\\n  -d 'username=admin' \\\n  -d 'password=admin_secret_2026' \\\n  -d 'grant_type=password' | python3 -c \"import json,sys; print(json.load(sys.stdin)['access_token'])\")\n\n# 2. Cr\u00e9er le realm \"mon-app\" avec dur\u00e9es de vie de tokens optimis\u00e9es\ncurl -s -X POST http:\/\/localhost:8080\/admin\/realms \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\n    \"realm\": \"mon-app\",\n    \"enabled\": true,\n    \"displayName\": \"Mon Application\",\n    \"sslRequired\": \"external\",\n    \"registrationAllowed\": false,\n    \"loginWithEmailAllowed\": true,\n    \"duplicateEmailsAllowed\": false,\n    \"resetPasswordAllowed\": true,\n    \"accessTokenLifespan\": 300,\n    \"ssoSessionIdleTimeout\": 1800,\n    \"ssoSessionMaxLifespan\": 36000,\n    \"refreshTokenMaxReuse\": 0,\n    \"bruteForceProtected\": true,\n    \"failureFactor\": 5,\n    \"waitIncrementSeconds\": 60\n  }'\n\n# 3. Cr\u00e9er un client confidentiel pour l'API Node.js\ncurl -s -X POST http:\/\/localhost:8080\/admin\/realms\/mon-app\/clients \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\n    \"clientId\": \"nodejs-api\",\n    \"name\": \"API Node.js\",\n    \"protocol\": \"openid-connect\",\n    \"publicClient\": false,\n    \"authorizationServicesEnabled\": false,\n    \"serviceAccountsEnabled\": true,\n    \"directAccessGrantsEnabled\": false,\n    \"standardFlowEnabled\": true,\n    \"implicitFlowEnabled\": false,\n    \"redirectUris\": [\"http:\/\/localhost:3000\/callback\", \"http:\/\/localhost:3000\/*\"],\n    \"webOrigins\": [\"http:\/\/localhost:3000\"],\n    \"attributes\": {\n      \"pkce.code.challenge.method\": \"S256\"\n    }\n  }'\n\necho \"Realm et client cr\u00e9\u00e9s avec succ\u00e8s\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Param\u00e8tre critique : <code>\"refreshTokenMaxReuse\": 0<\/code> active la rotation obligatoire des refresh tokens. Chaque utilisation d&#8217;un refresh token g\u00e9n\u00e8re un nouveau token et invalide imm\u00e9diatement l&#8217;ancien. Si Keycloak d\u00e9tecte qu&#8217;un refresh token d\u00e9j\u00e0 utilis\u00e9 est pr\u00e9sent\u00e9 \u00e0 nouveau (signe possible de vol), il r\u00e9voque toute la famille de tokens de la session concern\u00e9e. La dur\u00e9e de vie de l&#8217;access token est fix\u00e9e \u00e0 <strong>300 secondes<\/strong> (5 minutes), valeur recommand\u00e9e par l&#8217;ANSSI pour les API expos\u00e9es sur internet. Le param\u00e8tre <code>\"bruteForceProtected\": true<\/code> verrouille les comptes apr\u00e8s 5 tentatives \u00e9chou\u00e9es, avec un d\u00e9lai d&#8217;attente croissant de 60 secondes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-3-creer-les-utilisateurs-et-les-roles\">\u00c9tape 3 : Cr\u00e9er les utilisateurs et les r\u00f4les<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Les r\u00f4les Keycloak structurent les autorisations \u00e0 deux niveaux : les r\u00f4les de realm (globaux \u00e0 tous les clients) et les r\u00f4les de client (sp\u00e9cifiques \u00e0 une application). Pour une API REST, d\u00e9finissez vos r\u00f4les m\u00e9tier comme des r\u00f4les de realm. Cela simplifie la gestion des autorisations dans une architecture microservices o\u00f9 plusieurs clients partagent les m\u00eames r\u00f4les.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Cr\u00e9er un utilisateur de test avec mot de passe permanent\ncurl -s -X POST http:\/\/localhost:8080\/admin\/realms\/mon-app\/users \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\n    \"username\": \"alice\",\n    \"email\": \"alice@example.com\",\n    \"enabled\": true,\n    \"emailVerified\": true,\n    \"firstName\": \"Alice\",\n    \"lastName\": \"Dupont\",\n    \"credentials\": [{\n      \"type\": \"password\",\n      \"value\": \"MotDePasseTest2026!\",\n      \"temporary\": false\n    }]\n  }'\n\n# Cr\u00e9er les r\u00f4les m\u00e9tier du realm\nfor ROLE in \"lecteur\" \"editeur\" \"admin-api\"; do\n  curl -s -X POST http:\/\/localhost:8080\/admin\/realms\/mon-app\/roles \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -H 'Content-Type: application\/json' \\\n    -d \"{\\\"name\\\": \\\"$ROLE\\\", \\\"description\\\": \\\"R\u00f4le $ROLE pour l'API Node.js\\\"}\"\ndone\n\necho \"Utilisateur et r\u00f4les cr\u00e9\u00e9s\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La gestion des r\u00f4les dans Keycloak suit le mod\u00e8le RBAC (Role-Based Access Control). Dans votre API Node.js, vous v\u00e9rifierez la pr\u00e9sence de ces r\u00f4les dans le champ <code>realm_access.roles<\/code> du JWT d\u00e9cod\u00e9. Keycloak inclut automatiquement ces claims dans les access tokens. Pour les r\u00f4les sp\u00e9cifiques \u00e0 un client, ils apparaissent dans <code>resource_access.nodejs-api.roles<\/code>. Choisissez des r\u00f4les de realm pour des autorisations partag\u00e9es entre applications, et des r\u00f4les de client pour des autorisations propres \u00e0 une seule application.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-4-initialiser-le-projet-node-js\">\u00c9tape 4 : Initialiser le projet Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cr\u00e9ez la structure de votre projet avant d&#8217;installer les d\u00e9pendances. Une organisation claire facilite la maintenance et la revue de code dans les \u00e9quipes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Initialiser le projet Node.js\nmkdir keycloak-nodejs-api && cd keycloak-nodejs-api\nnpm init -y\n\n# Installer les d\u00e9pendances de production\nnpm install express@4.19.2 keycloak-connect@26.1.1 express-session@1.18.0 dotenv@16.4.5\n\n# Installer les outils de d\u00e9veloppement\nnpm install --save-dev nodemon@3.1.0 jest@29.7.0 supertest@7.0.0\n\n# Cr\u00e9er la structure du projet\nmkdir -p src\/{routes,middleware,config}\ntouch src\/server.js \\\n      src\/config\/keycloak.js \\\n      src\/middleware\/auth.js \\\n      src\/middleware\/pkce.js \\\n      src\/routes\/public.js \\\n      src\/routes\/protected.js \\\n      src\/routes\/auth.js \\\n      .env .env.example .gitignore\n\n# Ajouter .env au .gitignore\necho \".env\" >> .gitignore\necho \"node_modules\/\" >> .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Versions importantes \u00e0 retenir : <code>keycloak-connect@26.1.1<\/code> est la derni\u00e8re version stable align\u00e9e sur le serveur Keycloak 26.x. La version 26.1.1 r\u00e9sout un probl\u00e8me de validation de nonce OIDC pr\u00e9sent dans les versions 26.0.x. Utilisez toujours une version de <code>keycloak-connect<\/code> dont le num\u00e9ro de version majeur correspond \u00e0 celui de votre serveur Keycloak. Une version de middleware 25.x avec un serveur 26.x peut g\u00e9n\u00e9rer des erreurs de d\u00e9codage de configuration impr\u00e9vues.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fichier <code>.env<\/code> \u00e0 configurer, jamais \u00e0 committer dans votre d\u00e9p\u00f4t :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\"># .env - JAMAIS COMMITTER CE FICHIER (ajoutez .env dans .gitignore)\nKEYCLOAK_BASE_URL=http:\/\/localhost:8080\nKEYCLOAK_REALM=mon-app\nKEYCLOAK_CLIENT_ID=nodejs-api\nKEYCLOAK_CLIENT_SECRET=votre_secret_client_keycloak_ici\nSESSION_SECRET=une_chaine_aleatoire_de_64_caracteres_minimum_generee_avec_openssl_rand_hex_32\nPORT=3000\nNODE_ENV=development<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-5-configurer-le-middleware-keycloak-connect\">\u00c9tape 5 : Configurer le middleware keycloak-connect<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>keycloak-connect<\/code> est le middleware officiel Keycloak pour Express. Il g\u00e8re automatiquement la validation des tokens Bearer (signature cryptographique, expiration, issuer), la gestion des sessions et le rafra\u00eechissement transparent des access tokens expir\u00e9s. Contrairement \u00e0 une validation JWT statique manuelle, il peut \u00e9galement effectuer une v\u00e9rification de r\u00e9vocation via l&#8217;endpoint d&#8217;introspection.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/config\/keycloak.js\n'use strict';\nrequire('dotenv').config();\nconst Keycloak = require('keycloak-connect');\n\nlet _keycloak;\n\nconst keycloakConfig = {\n  realm: process.env.KEYCLOAK_REALM,\n  'auth-server-url': process.env.KEYCLOAK_BASE_URL + '\/',\n  'ssl-required': process.env.NODE_ENV === 'production' ? 'all' : 'external',\n  resource: process.env.KEYCLOAK_CLIENT_ID,\n  credentials: {\n    secret: process.env.KEYCLOAK_CLIENT_SECRET\n  },\n  'confidential-port': 0,\n  'bearer-only': true \/\/ Mode API REST : pas de redirection vers la page de login\n};\n\nfunction initKeycloak(memoryStore) {\n  if (_keycloak) {\n    console.warn('[KEYCLOAK] Instance d\u00e9j\u00e0 initialis\u00e9e, retour de l\\'instance existante');\n    return _keycloak;\n  }\n\n  console.log(`[KEYCLOAK] Connexion au realm \"${keycloakConfig.realm}\" sur ${process.env.KEYCLOAK_BASE_URL}`);\n  _keycloak = new Keycloak({ store: memoryStore }, keycloakConfig);\n\n  return _keycloak;\n}\n\nfunction getKeycloak() {\n  if (!_keycloak) {\n    throw new Error('Keycloak non initialis\u00e9. Appelez initKeycloak() d\\'abord.');\n  }\n  return _keycloak;\n}\n\nmodule.exports = { initKeycloak, getKeycloak };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le param\u00e8tre <code>'bearer-only': true<\/code> est fondamental pour une API REST. Sans ce param\u00e8tre, keycloak-connect tente de rediriger les requ\u00eates non authentifi\u00e9es vers la page de login Keycloak, comportement inadapt\u00e9 pour une API consomm\u00e9e par des clients programmatiques (applications mobiles, frontends React\/Vue, autres microservices). En mode <code>bearer-only<\/code>, les requ\u00eates sans token Bearer valide re\u00e7oivent directement une r\u00e9ponse <code>401 Unauthorized<\/code> avec le header <code>WWW-Authenticate: Bearer realm=\"mon-app\"<\/code>. Seul le param\u00e8tre <code>'auth-server-url'<\/code> doit se terminer par un slash.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-6-construire-le-serveur-express-principal\">\u00c9tape 6 : Construire le serveur Express principal<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/server.js\n'use strict';\nrequire('dotenv').config();\nconst express = require('express');\nconst session = require('express-session');\nconst { initKeycloak } = require('.\/config\/keycloak');\nconst publicRoutes = require('.\/routes\/public');\nconst protectedRoutes = require('.\/routes\/protected');\nconst authRoutes = require('.\/routes\/auth');\n\nconst app = express();\nconst PORT = parseInt(process.env.PORT || '3000', 10);\n\n\/\/ Store de sessions (remplacer par Redis en production)\nconst memoryStore = new session.MemoryStore();\n\n\/\/ Configuration des sessions (requise par keycloak-connect)\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  store: memoryStore,\n  name: 'kc.session', \/\/ Nom de cookie non g\u00e9n\u00e9rique\n  cookie: {\n    secure: process.env.NODE_ENV === 'production',\n    httpOnly: true,         \/\/ Protection XSS\n    sameSite: 'lax',       \/\/ Protection CSRF partielle\n    maxAge: 30 * 60 * 1000 \/\/ 30 minutes\n  }\n}));\n\n\/\/ Middleware Keycloak (doit \u00eatre apr\u00e8s session)\nconst keycloak = initKeycloak(memoryStore);\napp.use(keycloak.middleware({\n  logout: '\/logout',\n  admin: '\/'\n}));\n\n\/\/ Parsing JSON avec limite de taille\napp.use(express.json({ limit: '1mb' }));\n\n\/\/ En-t\u00eates de s\u00e9curit\u00e9 de base\napp.use((req, res, next) => {\n  res.setHeader('X-Content-Type-Options', 'nosniff');\n  res.setHeader('X-Frame-Options', 'DENY');\n  res.setHeader('X-XSS-Protection', '0'); \/\/ D\u00e9sactiv\u00e9 en faveur de CSP\n  next();\n});\n\n\/\/ Routes\napp.use('\/auth', authRoutes);\napp.use('\/api\/public', publicRoutes);\napp.use('\/api\/protected', protectedRoutes(keycloak));\n\n\/\/ Gestion d'erreurs globale (doit \u00eatre en dernier)\napp.use((err, req, res, next) => {\n  const status = err.status || 500;\n  console.error(`[ERROR] ${err.message} (${status})`);\n  res.status(status).json({\n    error: err.message || 'Erreur interne du serveur',\n    code: err.code || 'INTERNAL_ERROR'\n  });\n});\n\nconst server = app.listen(PORT, () => {\n  console.log(`[SERVER] D\u00e9marr\u00e9 sur le port ${PORT} (${process.env.NODE_ENV})`);\n  console.log(`[SERVER] Realm Keycloak: ${process.env.KEYCLOAK_REALM}`);\n});\n\nmodule.exports = { app, server };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-7-proteger-les-routes-avec-le-middleware-dauthentification\">\u00c9tape 7 : Prot\u00e9ger les routes avec le middleware d&#8217;authentification<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">keycloak-connect expose plusieurs m\u00e9thodes de protection des routes. <code>keycloak.protect()<\/code> exige un token Bearer valide. <code>keycloak.protect('realm:role')<\/code> exige un token valide et le r\u00f4le de realm sp\u00e9cifi\u00e9. <code>keycloak.protect('resource:scope')<\/code> applique les politiques de ressources Keycloak pour des contr\u00f4les d&#8217;acc\u00e8s fins (fine-grained authorization).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/routes\/protected.js\n'use strict';\nconst express = require('express');\n\nmodule.exports = function(keycloak) {\n  const router = express.Router();\n\n  \/\/ Accessible \u00e0 tout utilisateur authentifi\u00e9\n  router.get('\/profil',\n    keycloak.protect(),\n    (req, res) => {\n      const tokenContent = req.kauth.grant.access_token.content;\n      res.json({\n        sub: tokenContent.sub,\n        email: tokenContent.email,\n        nom: tokenContent.name,\n        prenom: tokenContent.given_name,\n        roles: tokenContent.realm_access?.roles || [],\n        sessionState: tokenContent.session_state,\n        expiresAt: new Date(tokenContent.exp * 1000).toISOString()\n      });\n    }\n  );\n\n  \/\/ Accessible uniquement aux \u00e9diteurs\n  router.get('\/articles',\n    keycloak.protect('realm:editeur'),\n    (req, res) => {\n      res.json({\n        articles: [\n          { id: 1, titre: 'S\u00e9curit\u00e9 OAuth 2.0 en 2026', statut: 'publi\u00e9' },\n          { id: 2, titre: 'DPoP et rotation de tokens', statut: 'brouillon' }\n        ]\n      });\n    }\n  );\n\n  \/\/ Route administrative avec v\u00e9rification de r\u00f4le personnalis\u00e9e\n  router.delete('\/articles\/:id',\n    keycloak.protect(),\n    (req, res, next) => {\n      const roles = req.kauth.grant.access_token.content.realm_access?.roles || [];\n\n      if (!roles.includes('admin-api')) {\n        return res.status(403).json({\n          error: 'Acc\u00e8s interdit',\n          code: 'INSUFFICIENT_ROLE',\n          requis: 'admin-api',\n          present: roles\n        });\n      }\n      next();\n    },\n    (req, res) => {\n      res.json({\n        supprime: true,\n        id: parseInt(req.params.id, 10),\n        par: req.kauth.grant.access_token.content.email\n      });\n    }\n  );\n\n  \/\/ Route avec informations du token pour le d\u00e9bogage (d\u00e9sactiver en production)\n  if (process.env.NODE_ENV !== 'production') {\n    router.get('\/debug\/token',\n      keycloak.protect(),\n      (req, res) => {\n        res.json(req.kauth.grant.access_token.content);\n      }\n    );\n  }\n\n  return router;\n};<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;objet <code>req.kauth.grant<\/code> inject\u00e9 par keycloak-connect donne acc\u00e8s \u00e0 l&#8217;int\u00e9gralit\u00e9 du grant OAuth 2.0. Le champ <code>access_token.content<\/code> contient le JWT d\u00e9cod\u00e9 avec les claims standard OIDC (<code>sub<\/code>, <code>email<\/code>, <code>name<\/code>, <code>iat<\/code>, <code>exp<\/code>) et les claims Keycloak sp\u00e9cifiques (<code>realm_access<\/code>, <code>resource_access<\/code>, <code>session_state<\/code>, <code>azp<\/code>). La route <code>\/debug\/token<\/code> est utile pendant le d\u00e9veloppement pour inspecter le contenu exact du token, mais doit \u00eatre d\u00e9sactiv\u00e9e en production car elle expose des m\u00e9tadonn\u00e9es de session.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-8-implementer-pkce-pour-le-flux-authorization-code\">\u00c9tape 8 : Impl\u00e9menter PKCE pour le flux Authorization Code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pour les applications web avec une interface utilisateur, vous devez impl\u00e9menter le flux Authorization Code complet avec PKCE. La mise en oeuvre suit la RFC 7636 \u00e0 la lettre, en utilisant uniquement le module <code>crypto<\/code> natif de Node.js, sans d\u00e9pendances tierces suppl\u00e9mentaires.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/middleware\/pkce.js\n'use strict';\nconst crypto = require('crypto');\n\n\/\/ G\u00e9n\u00e9rer un code verifier de 43 \u00e0 128 caract\u00e8res (base64url, RFC 7636 section 4.1)\nfunction generateCodeVerifier() {\n  return crypto.randomBytes(32).toString('base64url');\n}\n\n\/\/ D\u00e9river le code challenge avec SHA-256 (m\u00e9thode S256, RFC 7636 section 4.2)\nfunction generateCodeChallenge(verifier) {\n  return crypto.createHash('sha256').update(verifier).digest('base64url');\n}\n\n\/\/ Middleware : d\u00e9marrer le flux OAuth 2.0 + PKCE\nfunction startAuthFlow(req, res) {\n  const verifier = generateCodeVerifier();\n  const challenge = generateCodeChallenge(verifier);\n  const state = crypto.randomBytes(16).toString('hex');\n\n  \/\/ Stocker en session c\u00f4t\u00e9 serveur uniquement (jamais expos\u00e9 au client)\n  req.session.pkce_verifier = verifier;\n  req.session.oauth_state = state;\n  req.session.pkce_created_at = Date.now();\n\n  const params = new URLSearchParams({\n    client_id: process.env.KEYCLOAK_CLIENT_ID,\n    redirect_uri: `http:\/\/localhost:${process.env.PORT}\/auth\/callback`,\n    response_type: 'code',\n    scope: 'openid email profile',\n    state: state,\n    code_challenge: challenge,\n    code_challenge_method: 'S256'\n  });\n\n  const authUrl = `${process.env.KEYCLOAK_BASE_URL}\/realms\/${process.env.KEYCLOAK_REALM}\/protocol\/openid-connect\/auth?${params}`;\n  res.redirect(authUrl);\n}\n\n\/\/ \u00c9changer le code d'autorisation contre des tokens\nasync function exchangeCode(code, verifier) {\n  const tokenUrl = `${process.env.KEYCLOAK_BASE_URL}\/realms\/${process.env.KEYCLOAK_REALM}\/protocol\/openid-connect\/token`;\n\n  const response = await fetch(tokenUrl, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      grant_type: 'authorization_code',\n      client_id: process.env.KEYCLOAK_CLIENT_ID,\n      client_secret: process.env.KEYCLOAK_CLIENT_SECRET,\n      code: code,\n      redirect_uri: `http:\/\/localhost:${process.env.PORT}\/auth\/callback`,\n      code_verifier: verifier\n    })\n  });\n\n  if (!response.ok) {\n    const err = await response.json().catch(() => ({}));\n    throw Object.assign(\n      new Error(err.error_description || err.error || '\u00c9chec de l\\'\u00e9change de tokens'),\n      { status: response.status, code: err.error }\n    );\n  }\n\n  return response.json();\n}\n\nmodule.exports = { startAuthFlow, exchangeCode };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le param\u00e8tre <code>state<\/code> anti-CSRF est obligatoire. Si le state re\u00e7u dans le callback ne correspond pas au state stock\u00e9 en session, rejeter la requ\u00eate imm\u00e9diatement. Un state manquant ou incorrect indique une tentative d&#8217;attaque CSRF (un site malveillant force l&#8217;utilisateur \u00e0 initier un flux OAuth vers votre application) ou une confusion de flux. V\u00e9rifiez aussi que le challenge PKCE n&#8217;a pas plus de 10 minutes (<code>pkce_created_at<\/code>) pour limiter la fen\u00eatre d&#8217;attaque.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-9-traiter-le-callback-et-gerer-les-tokens\">\u00c9tape 9 : Traiter le callback et g\u00e9rer les tokens<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/routes\/auth.js\n'use strict';\nconst express = require('express');\nconst { startAuthFlow, exchangeCode } = require('..\/middleware\/pkce');\nconst router = express.Router();\n\n\/\/ D\u00e9marrer le flux OAuth 2.0\nrouter.get('\/login', startAuthFlow);\n\n\/\/ Traiter le callback Keycloak\nrouter.get('\/callback', async (req, res) => {\n  const { code, state, error, error_description } = req.query;\n\n  \/\/ V\u00e9rification anti-CSRF du state (obligatoire)\n  if (!state || state !== req.session.oauth_state) {\n    return res.status(400).json({\n      error: 'state_mismatch',\n      description: 'Param\u00e8tre state invalide ou absent'\n    });\n  }\n\n  \/\/ V\u00e9rifier que le challenge PKCE n'est pas expir\u00e9 (10 minutes max)\n  const pkceAge = Date.now() - (req.session.pkce_created_at || 0);\n  if (pkceAge > 10 * 60 * 1000) {\n    req.session.destroy();\n    return res.status(400).json({ error: 'pkce_expired', action: 'relogin' });\n  }\n\n  if (error) {\n    return res.status(400).json({ error, description: error_description });\n  }\n\n  if (!code) {\n    return res.status(400).json({ error: 'missing_code' });\n  }\n\n  try {\n    const tokens = await exchangeCode(code, req.session.pkce_verifier);\n\n    \/\/ Nettoyer les donn\u00e9es PKCE de la session imm\u00e9diatement apr\u00e8s utilisation\n    delete req.session.pkce_verifier;\n    delete req.session.oauth_state;\n    delete req.session.pkce_created_at;\n\n    \/\/ Stocker les tokens c\u00f4t\u00e9 serveur (jamais envoy\u00e9s au client JS)\n    req.session.access_token = tokens.access_token;\n    req.session.refresh_token = tokens.refresh_token;\n    req.session.id_token = tokens.id_token;\n    req.session.token_expires_at = Date.now() + (tokens.expires_in * 1000);\n\n    res.json({\n      message: 'Authentification r\u00e9ussie',\n      expires_in: tokens.expires_in,\n      token_type: tokens.token_type,\n      scope: tokens.scope\n    });\n  } catch (err) {\n    console.error('[AUTH CALLBACK]', err.message);\n    res.status(err.status || 401).json({\n      error: err.code || 'auth_failed',\n      description: err.message\n    });\n  }\n});\n\n\/\/ Rafra\u00eechir les tokens (rotation activ\u00e9e)\nrouter.post('\/refresh', async (req, res) => {\n  const refreshToken = req.session.refresh_token;\n\n  if (!refreshToken) {\n    return res.status(401).json({ error: 'no_refresh_token', action: 'relogin' });\n  }\n\n  const tokenUrl = `${process.env.KEYCLOAK_BASE_URL}\/realms\/${process.env.KEYCLOAK_REALM}\/protocol\/openid-connect\/token`;\n\n  try {\n    const response = await fetch(tokenUrl, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n      body: new URLSearchParams({\n        grant_type: 'refresh_token',\n        client_id: process.env.KEYCLOAK_CLIENT_ID,\n        client_secret: process.env.KEYCLOAK_CLIENT_SECRET,\n        refresh_token: refreshToken\n      })\n    });\n\n    if (!response.ok) {\n      \/\/ Refresh token r\u00e9voqu\u00e9 ou expir\u00e9 : invalider la session\n      req.session.destroy();\n      return res.status(401).json({ error: 'refresh_invalid', action: 'relogin' });\n    }\n\n    const tokens = await response.json();\n\n    \/\/ Remplacer TOUS les tokens (rotation)\n    req.session.access_token = tokens.access_token;\n    req.session.refresh_token = tokens.refresh_token; \/\/ Nouveau refresh token (rotation)\n    req.session.token_expires_at = Date.now() + (tokens.expires_in * 1000);\n\n    res.json({\n      message: 'Tokens rafra\u00eechis',\n      expires_in: tokens.expires_in\n    });\n  } catch (err) {\n    console.error('[REFRESH ERROR]', err.message);\n    res.status(503).json({ error: 'refresh_unavailable' });\n  }\n});\n\n\/\/ D\u00e9connexion avec r\u00e9vocation\nrouter.post('\/logout', async (req, res) => {\n  const idToken = req.session.id_token;\n\n  if (idToken) {\n    const logoutUrl = `${process.env.KEYCLOAK_BASE_URL}\/realms\/${process.env.KEYCLOAK_REALM}\/protocol\/openid-connect\/logout`;\n    await fetch(logoutUrl, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n      body: new URLSearchParams({\n        client_id: process.env.KEYCLOAK_CLIENT_ID,\n        client_secret: process.env.KEYCLOAK_CLIENT_SECRET,\n        id_token_hint: idToken\n      })\n    }).catch(err => console.error('[LOGOUT ERROR]', err.message));\n  }\n\n  req.session.destroy();\n  res.json({ message: 'D\u00e9connexion r\u00e9ussie' });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-10-validation-par-introspection-et-revocation-des-tokens\">\u00c9tape 10 : Validation par introspection et r\u00e9vocation des tokens<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La validation statique d&#8217;un JWT (v\u00e9rification de la signature et de l&#8217;expiration localement) ne suffit pas dans un sc\u00e9nario de r\u00e9vocation. Si un administrateur r\u00e9voque manuellement les sessions d&#8217;un utilisateur dans Keycloak, son access token reste valide localement jusqu&#8217;\u00e0 son expiration naturelle (300 secondes). Pour les actions sensibles, l&#8217;introspection en temps r\u00e9el est indispensable.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/middleware\/auth.js\n'use strict';\n\n\/\/ Middleware d'introspection temps r\u00e9el (pour les routes critiques)\nasync function introspectToken(req, res, next) {\n  const authHeader = req.headers.authorization;\n\n  if (!authHeader?.startsWith('Bearer ')) {\n    return res.status(401).json({\n      error: 'missing_token',\n      description: 'Header Authorization manquant ou malform\u00e9. Format attendu: Bearer <token>'\n    });\n  }\n\n  const token = authHeader.slice(7); \/\/ Retirer \"Bearer \"\n\n  const introspectUrl = `${process.env.KEYCLOAK_BASE_URL}\/realms\/${process.env.KEYCLOAK_REALM}\/protocol\/openid-connect\/token\/introspect`;\n\n  try {\n    const response = await fetch(introspectUrl, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application\/x-www-form-urlencoded',\n        \/\/ Authentification client via Basic Auth (client_id:client_secret)\n        'Authorization': 'Basic ' + Buffer.from(\n          `${process.env.KEYCLOAK_CLIENT_ID}:${process.env.KEYCLOAK_CLIENT_SECRET}`\n        ).toString('base64')\n      },\n      body: new URLSearchParams({ token }),\n      signal: AbortSignal.timeout(5000) \/\/ Timeout 5 secondes\n    });\n\n    if (!response.ok) {\n      throw new Error(`Keycloak introspection HTTP ${response.status}`);\n    }\n\n    const data = await response.json();\n\n    if (!data.active) {\n      return res.status(401).json({\n        error: 'token_inactive',\n        description: 'Token r\u00e9voqu\u00e9, expir\u00e9 ou invalide'\n      });\n    }\n\n    \/\/ Attacher les claims v\u00e9rifi\u00e9s \u00e0 la requ\u00eate\n    req.tokenClaims = data;\n    next();\n\n  } catch (err) {\n    if (err.name === 'TimeoutError') {\n      console.error('[INTROSPECTION TIMEOUT]');\n      return res.status(503).json({ error: 'auth_service_timeout' });\n    }\n    \/\/ Fail-secure : en cas d'erreur, refuser par d\u00e9faut\n    console.error('[INTROSPECTION ERROR]', err.message);\n    res.status(503).json({\n      error: 'auth_service_unavailable',\n      description: 'Service d\\'authentification temporairement indisponible'\n    });\n  }\n}\n\nmodule.exports = { introspectToken };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La strat\u00e9gie <em>fail-secure<\/em> dans le bloc catch est une d\u00e9cision de conception d\u00e9lib\u00e9r\u00e9e : si Keycloak est inaccessible, le middleware refuse toutes les requ\u00eates plut\u00f4t que de les autoriser. Cette approche est recommand\u00e9e pour les APIs manipulant des donn\u00e9es sensibles (donn\u00e9es personnelles, paiements, actions irr\u00e9versibles). Pour les APIs \u00e0 haute disponibilit\u00e9 o\u00f9 la disponibilit\u00e9 prime, impl\u00e9mentez un fallback sur validation locale de signature JWT avec v\u00e9rification des JWKS, mais uniquement pour les tokens r\u00e9cents (moins de 60 secondes) qui ne peuvent pas encore avoir \u00e9t\u00e9 r\u00e9voqu\u00e9s.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-11-securisation-cors-et-en-tetes-de-securite\">\u00c9tape 11 : S\u00e9curisation CORS et en-t\u00eates de s\u00e9curit\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Une API Keycloak mal configur\u00e9e c\u00f4t\u00e9 CORS peut exposer vos tokens \u00e0 des sites malveillants. La configuration CORS doit \u00eatre restrictive par d\u00e9faut et ouverte uniquement aux origines l\u00e9gitimes. Ne configurez jamais <code>Access-Control-Allow-Origin: *<\/code> avec <code>Access-Control-Allow-Credentials: true<\/code>, combinaison interdite par les navigateurs modernes et dangereuse pour la s\u00e9curit\u00e9 des tokens.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ src\/middleware\/security.js\n'use strict';\n\nconst ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'http:\/\/localhost:3000')\n  .split(',')\n  .map(o => o.trim());\n\nfunction corsMiddleware(req, res, next) {\n  const origin = req.headers.origin;\n\n  if (origin && ALLOWED_ORIGINS.includes(origin)) {\n    res.setHeader('Access-Control-Allow-Origin', origin);\n    res.setHeader('Vary', 'Origin');\n  }\n\n  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n  res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Request-ID');\n  res.setHeader('Access-Control-Allow-Credentials', 'true');\n  res.setHeader('Access-Control-Max-Age', '86400');\n\n  \/\/ En-t\u00eates de s\u00e9curit\u00e9 HTTP\n  res.setHeader('X-Content-Type-Options', 'nosniff');\n  res.setHeader('X-Frame-Options', 'DENY');\n  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');\n  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');\n\n  if (req.method === 'OPTIONS') {\n    return res.sendStatus(204);\n  }\n\n  next();\n}\n\n\/\/ Logger s\u00e9curis\u00e9 : jamais de tokens ou de donn\u00e9es sensibles dans les logs\nfunction secureLogger(req, res, next) {\n  const start = process.hrtime.bigint();\n  const requestId = req.headers['x-request-id'] || crypto.randomUUID();\n\n  res.on('finish', () => {\n    const duration = Number(process.hrtime.bigint() - start) \/ 1e6;\n    \/\/ Format: [timestamp] METHOD path status duration requestId\n    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} ${res.statusCode} ${duration.toFixed(1)}ms req=${requestId}`);\n    \/\/ JAMAIS logger: req.headers.authorization, req.body.password, tokens\n  });\n\n  next();\n}\n\nmodule.exports = { corsMiddleware, secureLogger };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"etape-12-tester-lintegration-avec-curl-et-jest\">\u00c9tape 12 : Tester l&#8217;int\u00e9gration avec curl et Jest<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de d\u00e9ployer, validez l&#8217;int\u00e9gration compl\u00e8te : authentification r\u00e9ussie, acc\u00e8s aux routes prot\u00e9g\u00e9es, refus des acc\u00e8s non autoris\u00e9s, rafra\u00eechissement de tokens et r\u00e9vocation. Ces tests couvrent les chemins critiques de votre API.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\"># Tests d'int\u00e9gration via curl\n\n# 1. Obtenir un token via Client Credentials Grant (service-to-service)\nTOKEN_RESPONSE=$(curl -s -X POST \\\n  \"${KEYCLOAK_BASE_URL}\/realms\/${KEYCLOAK_REALM}\/protocol\/openid-connect\/token\" \\\n  -H 'Content-Type: application\/x-www-form-urlencoded' \\\n  -d \"client_id=${KEYCLOAK_CLIENT_ID}\" \\\n  -d \"client_secret=${KEYCLOAK_CLIENT_SECRET}\" \\\n  -d 'grant_type=client_credentials' \\\n  -d 'scope=openid')\n\nACCESS_TOKEN=$(echo $TOKEN_RESPONSE | python3 -c \"import json,sys; print(json.load(sys.stdin).get('access_token', 'ERREUR'))\")\necho \"Access token: ${ACCESS_TOKEN:0:60}...\"\necho \"Expire dans: $(echo $TOKEN_RESPONSE | python3 -c \"import json,sys; print(json.load(sys.stdin).get('expires_in', 0))\") secondes\"\n\n# 2. Acc\u00e9der \u00e0 une route prot\u00e9g\u00e9e (doit retourner 200)\necho \"\\n--- Test route prot\u00e9g\u00e9e ---\"\ncurl -s -X GET http:\/\/localhost:3000\/api\/protected\/profil \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" | python3 -m json.tool\n\n# 3. Acc\u00e8s sans token (doit retourner 401)\necho \"\\n--- Test sans token (doit \u00eatre 401) ---\"\nSTATUS=$(curl -s -o \/dev\/null -w \"%{http_code}\" http:\/\/localhost:3000\/api\/protected\/profil)\necho \"Status: $STATUS (attendu: 401)\"\n\n# 4. Token invalide (doit retourner 401)\necho \"\\n--- Test token invalide (doit \u00eatre 401) ---\"\nSTATUS=$(curl -s -o \/dev\/null -w \"%{http_code}\" http:\/\/localhost:3000\/api\/protected\/profil \\\n  -H \"Authorization: Bearer token_invalide_test\")\necho \"Status: $STATUS (attendu: 401)\"\n\n# 5. Introspection directe\necho \"\\n--- Introspection du token ---\"\ncurl -s -X POST \\\n  \"${KEYCLOAK_BASE_URL}\/realms\/${KEYCLOAK_REALM}\/protocol\/openid-connect\/token\/introspect\" \\\n  -u \"${KEYCLOAK_CLIENT_ID}:${KEYCLOAK_CLIENT_SECRET}\" \\\n  -d \"token=${ACCESS_TOKEN}\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(f'active={d[\\\"active\\\"]}, client={d.get(\\\"client_id\\\")}, exp={d.get(\\\"exp\\\")}')\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Sortie attendue pour l&#8217;introspection d&#8217;un token valide :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">{\n  \"active\": true,\n  \"sub\": \"nodejs-api\",\n  \"client_id\": \"nodejs-api\",\n  \"username\": \"service-account-nodejs-api\",\n  \"token_type\": \"Bearer\",\n  \"scope\": \"openid email profile\",\n  \"exp\": 1750416300,\n  \"iat\": 1750416000,\n  \"iss\": \"http:\/\/localhost:8080\/realms\/mon-app\",\n  \"jti\": \"5a3b2c1d-0e9f-8a7b-6c5d-4e3f2a1b0c9d\",\n  \"realm_access\": {\n    \"roles\": [\"offline_access\", \"uma_authorization\", \"default-roles-mon-app\"]\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"5-pieges-courants-et-comment-les-eviter\">5 pi\u00e8ges courants et comment les \u00e9viter<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-1-stocker-le-refresh-token-cote-client-javascript\">Pi\u00e8ge 1 : Stocker le refresh token c\u00f4t\u00e9 client JavaScript<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Stocker le refresh token dans le localStorage ou un cookie sans l&#8217;attribut <code>httpOnly<\/code> expose l&#8217;utilisateur aux attaques XSS. Un seul script malveillant inject\u00e9 dans votre page suffit \u00e0 voler le refresh token et maintenir un acc\u00e8s permanent au compte, m\u00eame apr\u00e8s expiration de l&#8217;access token. La solution est cat\u00e9gorique : les refresh tokens ne doivent jamais quitter le serveur. Stockez-les uniquement dans la session c\u00f4t\u00e9 serveur (objet <code>req.session<\/code> de Express). Le client ne re\u00e7oit que l&#8217;access token, dont la dur\u00e9e de vie limit\u00e9e \u00e0 300 secondes r\u00e9duit drastiquement l&#8217;impact en cas de vol.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-2-negliger-la-verification-de-lissuer-du-jwt\">Pi\u00e8ge 2 : N\u00e9gliger la v\u00e9rification de l&#8217;Issuer du JWT<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Un token JWT cryptographiquement valide peut provenir d&#8217;un realm Keycloak diff\u00e9rent du v\u00f4tre. Si vous acceptez des tokens sans v\u00e9rifier que le champ <code>iss<\/code> (issuer) correspond exactement \u00e0 <code>http:\/\/keycloak-host\/realms\/mon-app<\/code>, vous devenez vuln\u00e9rable aux attaques de confusion d&#8217;issuer : un attaquant cr\u00e9e son propre realm Keycloak avec les m\u00eames cl\u00e9s et g\u00e9n\u00e8re des tokens accept\u00e9s par votre API. keycloak-connect effectue cette v\u00e9rification automatiquement gr\u00e2ce \u00e0 la configuration du realm. Si vous impl\u00e9mentez une validation JWT manuelle avec <code>jsonwebtoken<\/code>, le param\u00e8tre <code>issuer<\/code> de la fonction <code>verify()<\/code> est obligatoire.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-3-utiliser-start-dev-en-environnement-de-staging-ou-production\">Pi\u00e8ge 3 : Utiliser start-dev en environnement de staging ou production<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le mode <code>start-dev<\/code> de Keycloak d\u00e9sactive TLS (toutes les communications en HTTP clair), utilise une base H2 en m\u00e9moire (toutes les donn\u00e9es effac\u00e9es au red\u00e9marrage), active des endpoints de diagnostic et des caches minimaux non adapt\u00e9s \u00e0 la charge. Plusieurs \u00e9quipes ont d\u00e9ploy\u00e9 en staging avec <code>start-dev<\/code> et d\u00e9couvert des pertes de configuration enti\u00e8res au red\u00e9marrage de Kubernetes. En production et staging, utilisez <code>start<\/code> avec les variables <code>KC_HOSTNAME<\/code>, <code>KC_DB<\/code>, <code>KC_HTTPS_CERTIFICATE_FILE<\/code> et <code>KC_HTTPS_CERTIFICATE_KEY_FILE<\/code> correctement configur\u00e9es. La commande de build optimis\u00e9e (<code>build<\/code>) avant <code>start<\/code> r\u00e9duit le temps de d\u00e9marrage de 60 \u00e0 80%.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-4-ignorer-la-revocation-en-temps-reel-pour-les-actions-critiques\">Pi\u00e8ge 4 : Ignorer la r\u00e9vocation en temps r\u00e9el pour les actions critiques<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Valider un JWT localement (v\u00e9rification de signature + expiration en m\u00e9moire) ne d\u00e9tecte pas la r\u00e9vocation. Si un administrateur d\u00e9sactive un compte utilisateur dans Keycloak \u00e0 14h00, l&#8217;access token de cet utilisateur reste valide localement jusqu&#8217;\u00e0 14h05 (300 secondes). Pour les routes manipulant des donn\u00e9es sensibles (suppression de compte, transfert d&#8217;argent, modification d&#8217;autorisations), l&#8217;introspection en temps r\u00e9el via l&#8217;endpoint Keycloak est obligatoire. Le surco\u00fbt de latence est d&#8217;environ 5 \u00e0 20 millisecondes selon la localisation de Keycloak, acceptable pour les op\u00e9rations critiques. Pour les routes en lecture seule \u00e0 faible risque, la validation locale est suffisante.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"piege-5-loguer-les-access-tokens-dans-les-fichiers-de-logs-applicatifs\">Pi\u00e8ge 5 : Loguer les access tokens dans les fichiers de logs applicatifs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Les access tokens JWT contiennent des claims sensibles (email, r\u00f4les, identifiants uniques) et donnent acc\u00e8s \u00e0 toutes les ressources prot\u00e9g\u00e9es pendant leur dur\u00e9e de vie. Plusieurs incidents de s\u00e9curit\u00e9 document\u00e9s ont \u00e9t\u00e9 caus\u00e9s par des tokens entiers loggu\u00e9s dans des syst\u00e8mes d&#8217;agr\u00e9gation de logs comme ELK ou Splunk, accessibles \u00e0 de nombreux op\u00e9rateurs. La r\u00e8gle : ne loguer jamais <code>req.headers.authorization<\/code> en entier. Si vous devez identifier un token dans les logs pour d\u00e9bogage, loggez uniquement les 12 premiers caract\u00e8res (<code>token.substring(0, 12) + '...'<\/code>) ou le champ <code>jti<\/code> (identifiant unique du token, sans valeur d&#8217;acc\u00e8s). N&#8217;incluez jamais les refresh tokens ni les secrets client dans aucun log.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"guide-de-depannage-10-problemes-et-leurs-solutions\">Guide de d\u00e9pannage : 10 probl\u00e8mes et leurs solutions<\/h2>\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><code>401 Unauthorized: token_not_provided<\/code><\/td><td>Header Authorization absent ou format incorrect<\/td><td>V\u00e9rifier le format exact : <code>Authorization: Bearer &lt;token&gt;<\/code> (sensible aux espaces)<\/td><\/tr><tr><td><code>401: Token is not active<\/code><\/td><td>Token expir\u00e9 (apr\u00e8s 300 secondes) ou r\u00e9voqu\u00e9 par admin<\/td><td>Appeler <code>POST \/auth\/refresh<\/code>, sinon rediriger vers le login<\/td><\/tr><tr><td><code>403: Access denied<\/code><\/td><td>R\u00f4le requis absent du token<\/td><td>Assigner le r\u00f4le \u00e0 l&#8217;utilisateur dans l&#8217;admin Keycloak, les tokens actuels ne seront pas mis \u00e0 jour imm\u00e9diatement (attendre expiration)<\/td><\/tr><tr><td><code>ECONNREFUSED localhost:8080<\/code><\/td><td>Keycloak non d\u00e9marr\u00e9 ou port erron\u00e9<\/td><td>V\u00e9rifier <code>docker ps<\/code>, attendre <code>Keycloak started<\/code> dans les logs, v\u00e9rifier <code>KEYCLOAK_BASE_URL<\/code><\/td><\/tr><tr><td><code>CORS: No Access-Control-Allow-Origin<\/code><\/td><td>Origine non list\u00e9e dans <code>ALLOWED_ORIGINS<\/code> ou Web Origins du client<\/td><td>Ajouter l&#8217;URL compl\u00e8te (avec port) dans <code>ALLOWED_ORIGINS<\/code> et dans Web Origins du client Keycloak<\/td><\/tr><tr><td><code>invalid_grant: Code not valid<\/code><\/td><td>Code d&#8217;autorisation d\u00e9j\u00e0 \u00e9chang\u00e9 ou expir\u00e9 (60 secondes par d\u00e9faut)<\/td><td>V\u00e9rifier les rechargements de page sur \/callback, \u00e9viter de traiter le callback deux fois<\/td><\/tr><tr><td><code>invalid_grant: Invalid refresh token<\/code><\/td><td>Refresh token r\u00e9voqu\u00e9 (rotation) ou session SSO expir\u00e9e<\/td><td>D\u00e9truire la session (<code>req.session.destroy()<\/code>) et relancer l&#8217;authentification<\/td><\/tr><tr><td><code>SSL_ERROR_RX_RECORD_TOO_LONG<\/code><\/td><td>Application tente HTTPS mais Keycloak r\u00e9pond en HTTP<\/td><td>V\u00e9rifier que <code>KEYCLOAK_BASE_URL<\/code> commence par <code>http:\/\/<\/code> en d\u00e9veloppement<\/td><\/tr><tr><td><code>Session not found<\/code> apr\u00e8s red\u00e9marrage<\/td><td>MemoryStore vide apr\u00e8s red\u00e9marrage du processus Node.js<\/td><td>Utiliser <code>connect-redis<\/code> en production : <code>npm install connect-redis redis<\/code><\/td><\/tr><tr><td><code>Realm does not exist<\/code><\/td><td>Nom de realm incorrect (sensible \u00e0 la casse)<\/td><td>V\u00e9rifier <code>KEYCLOAK_REALM<\/code> dans <code>.env<\/code>. Tester avec : <code>curl http:\/\/localhost:8080\/realms\/mon-app\/.well-known\/openid-configuration<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conseils-avances-pour-la-production\">Conseils avanc\u00e9s pour la production<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Persister les sessions avec Redis.<\/strong> Le <code>MemoryStore<\/code> perd toutes les sessions au red\u00e9marrage et ne scale pas horizontalement entre plusieurs instances Node.js. En production, installez <code>connect-redis<\/code> : <code>npm install connect-redis redis<\/code>. Configurez <code>new RedisStore({ client: redisClient, prefix: 'kc:', ttl: 1800 })<\/code>. Redis assure la persistance des sessions entre les red\u00e9marrages et permet le scale horizontal avec une architecture multi-instances derri\u00e8re un load balancer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Mettre en cache les cl\u00e9s publiques JWKS.<\/strong> Keycloak expose ses cl\u00e9s publiques de signature sur l&#8217;endpoint JWKS : <code>\/realms\/{realm}\/protocol\/openid-connect\/certs<\/code>. Pour la validation locale des signatures JWT (sans introspection), t\u00e9l\u00e9chargez ces cl\u00e9s au d\u00e9marrage et rafra\u00eechissez-les toutes les 24 heures ou imm\u00e9diatement si une validation de signature \u00e9choue (pour g\u00e9rer la rotation des cl\u00e9s Keycloak). Le package <code>jwks-rsa<\/code> automatise ce m\u00e9canisme avec un cache LRU configurable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Impl\u00e9menter un circuit breaker sur les appels Keycloak.<\/strong> Si Keycloak est indisponible pendant 30 secondes, vos routes prot\u00e9g\u00e9es retournent 503 en cascade. Impl\u00e9mentez un circuit breaker avec le package <code>opossum<\/code> pour distinguer les pannes transitoires (circuit ferm\u00e9, retries exponentiels) des pannes prolong\u00e9es (circuit ouvert, fail-fast imm\u00e9diat). Configurez 5 \u00e9checs cons\u00e9cutifs pour ouvrir le circuit avec un d\u00e9lai de r\u00e9cup\u00e9ration de 30 secondes. Cela r\u00e9duit la charge sur un Keycloak en difficult\u00e9 pendant sa r\u00e9cup\u00e9ration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Surveiller les m\u00e9triques Keycloak avec Prometheus.<\/strong> Keycloak 26.6.0 supporte nativement OpenTelemetry. Exposez les m\u00e9triques en activant <code>KC_METRICS_ENABLED=true<\/code>. Les alertes prioritaires \u00e0 configurer : <code>keycloak_failed_login_attempts_total<\/code> (seuil : 10 par minute par IP, alerte : attaque bruteforce), <code>keycloak_logins_total<\/code> (chute soudaine indiquant une panne), et <code>keycloak_client_login_attempts_total<\/code> (pic anormal indiquant un client compromis). Int\u00e9grez ces m\u00e9triques dans Grafana avec les datasources Prometheus pour une observabilit\u00e9 compl\u00e8te.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Configurer le backchannel logout OIDC.<\/strong> Le backchannel logout permet \u00e0 Keycloak de notifier votre API Node.js quand un utilisateur se d\u00e9connecte depuis n&#8217;importe quelle application du realm. Configurez <code>Backchannel Logout URL: http:\/\/votre-api.fr\/auth\/backchannel-logout<\/code> dans les param\u00e8tres Advanced de votre client Keycloak. Votre endpoint re\u00e7oit un <code>logout_token<\/code> JWT sign\u00e9 contenant le <code>sid<\/code> (session ID) \u00e0 invalider. Cela garantit la d\u00e9connexion synchronis\u00e9e entre toutes les applications du m\u00eame SSO.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>S\u00e9parer les clients Keycloak par environnement.<\/strong> Cr\u00e9ez un client distinct pour chaque environnement (d\u00e9veloppement, staging, production). Les redirectURIs, secrets et politiques de s\u00e9curit\u00e9 sont diff\u00e9rents par environnement. Un secret de d\u00e9veloppement compromis (pr\u00e9sent dans des logs CI\/CD ou un d\u00e9p\u00f4t Git mal configur\u00e9) n&#8217;affecte pas la production. En production, d\u00e9sactivez le flux Resource Owner Password Credentials (<code>\"directAccessGrantsEnabled\": false<\/code>) qui expose les mots de passe utilisateurs \u00e0 l&#8217;application cliente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"comparatif-des-strategies-dauthentification-node-js-en-2026\">Comparatif des strat\u00e9gies d&#8217;authentification Node.js en 2026<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Strat\u00e9gie<\/th><th>Complexit\u00e9 setup<\/th><th>Scalabilit\u00e9<\/th><th>R\u00e9vocation temps r\u00e9el<\/th><th>SSO multi-apps<\/th><th>Co\u00fbt<\/th><\/tr><\/thead><tbody><tr><td>Keycloak + keycloak-connect<\/td><td>Moyenne<\/td><td>Excellente<\/td><td>Oui (introspection)<\/td><td>Oui, natif<\/td><td>Gratuit (open source)<\/td><\/tr><tr><td>JWT stateless seul<\/td><td>Faible<\/td><td>Excellente<\/td><td>Non (stateless)<\/td><td>Non<\/td><td>Gratuit<\/td><\/tr><tr><td>Auth0 + express-jwt<\/td><td>Faible<\/td><td>Excellente<\/td><td>Oui<\/td><td>Oui<\/td><td>23 \u20ac\/mois\/1 000 MAU<\/td><\/tr><tr><td>Sessions Express seules<\/td><td>Faible<\/td><td>Limit\u00e9e (sticky)<\/td><td>Oui<\/td><td>Non<\/td><td>Gratuit<\/td><\/tr><tr><td>Passport.js local<\/td><td>Faible<\/td><td>Moyenne<\/td><td>Partielle<\/td><td>Non<\/td><td>Gratuit<\/td><\/tr><tr><td>Supabase Auth<\/td><td>Tr\u00e8s faible<\/td><td>Excellente<\/td><td>Oui<\/td><td>Non<\/td><td>0 \u00e0 25 \u20ac\/mois<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conformite-rgpd-et-souverainete-des-donnees-en-europe\">Conformit\u00e9 RGPD et souverainet\u00e9 des donn\u00e9es en Europe<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Keycloak est particuli\u00e8rement adapt\u00e9 aux organisations europ\u00e9ennes soumises au RGPD. Contrairement aux solutions SaaS comme Auth0 (Okta, \u00c9tats-Unis) ou Firebase Auth (Google, \u00c9tats-Unis) qui stockent les donn\u00e9es d&#8217;identit\u00e9 sur des serveurs am\u00e9ricains, Keycloak peut \u00eatre h\u00e9berg\u00e9 int\u00e9gralement dans des datacenters europ\u00e9ens, y compris sur des infrastructures certifi\u00e9es SecNumCloud pour les entit\u00e9s soumises aux exigences de la cybers\u00e9curit\u00e9 nationale fran\u00e7aise (OIV, OSE, administrations publiques).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keycloak supporte nativement les fonctionnalit\u00e9s requises par le RGPD. La suppression compl\u00e8te des donn\u00e9es utilisateur r\u00e9pond au droit \u00e0 l&#8217;effacement (article 17 RGPD) : un appel API supprime toutes les sessions, tokens, attributs et l&#8217;historique d&#8217;un utilisateur. L&#8217;export des donn\u00e9es personnelles r\u00e9pond au droit \u00e0 la portabilit\u00e9 (article 20). Les journaux d&#8217;audit des connexions et des actions administratives, conserv\u00e9s pendant une dur\u00e9e configurable, r\u00e9pondent aux obligations de tra\u00e7abilit\u00e9. Activez l&#8217;audit dans Realm Settings, onglet Events, en cochant &#8220;Save Events&#8221; avec une r\u00e9tention de 30 jours minimum.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La CVE-2026-0707 (contournement de contr\u00f4les de s\u00e9curit\u00e9 via manipulation du header Authorization, corrig\u00e9e dans 26.5.4) et la CVE-2026-2575 (d\u00e9ni de service par d\u00e9compression excessive de requ\u00eates SAML, corrig\u00e9e dans 26.5.4) illustrent l&#8217;importance d&#8217;une maintenance active. Keycloak publie 4 versions mineures par an selon un calendrier public. Abonnez-vous aux annonces de s\u00e9curit\u00e9 sur <a href=\"https:\/\/www.keycloak.org\/blog\" target=\"_blank\" rel=\"noopener noreferrer\">le blog officiel Keycloak<\/a> et configurez un pipeline de mise \u00e0 jour automatique en environnement de staging pour valider chaque nouvelle version avant d\u00e9ploiement en production.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ressources-et-references-officielles\">Ressources et r\u00e9f\u00e9rences officielles<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pour approfondir l&#8217;int\u00e9gration Keycloak et les standards OAuth 2.0\/OIDC, ces ressources font autorit\u00e9. La <a href=\"https:\/\/www.keycloak.org\/guides\" target=\"_blank\" rel=\"noopener noreferrer\">documentation officielle Keycloak<\/a> couvre l&#8217;ensemble des fonctionnalit\u00e9s avec des guides par cas d&#8217;usage. La <a href=\"https:\/\/openid.net\/connect\/\" target=\"_blank\" rel=\"noopener noreferrer\">sp\u00e9cification OpenID Connect<\/a> d\u00e9taille le protocole de couche d&#8217;identit\u00e9. La <a href=\"https:\/\/openid.net\/specs\/openid-connect-core-1_0.html\" target=\"_blank\" rel=\"noopener noreferrer\">sp\u00e9cification OIDC Core 1.0<\/a> d\u00e9crit les flows et les claims. Le <a href=\"https:\/\/github.com\/keycloak\/keycloak\" target=\"_blank\" rel=\"noopener noreferrer\">d\u00e9p\u00f4t GitHub de Keycloak<\/a> (plus de 23 000 \u00e9toiles) est la r\u00e9f\u00e9rence pour les issues connues et les contributions. L&#8217;<a href=\"https:\/\/oauth.net\/2\/pkce\/\" target=\"_blank\" rel=\"noopener noreferrer\">explication PKCE sur oauth.net<\/a> vulgarise la RFC 7636 avec des sch\u00e9mas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"couverture-connexe\">Couverture connexe<\/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 dans Node.js : 10 \u00c9tapes [2026]<\/a> &#8211; Comprendre la validation statique des JWT avant d&#8217;aborder l&#8217;introspection Keycloak<\/li>\n<li><a href=\"\/fr\/nodejs-session-management\/\">Gestion des Sessions Node.js : 11 \u00c9tapes [2026]<\/a> &#8211; Configuration des stores de sessions persistants pour keycloak-connect en production<\/li>\n<li><a href=\"\/fr\/security-headers-nodejs\/\">En-t\u00eates de S\u00e9curit\u00e9 HTTP dans Node.js : 12 \u00c9tapes [2026]<\/a> &#8211; Compl\u00e9ter la s\u00e9curisation de votre API Express avec des headers HTTP appropri\u00e9s<\/li>\n<li><a href=\"\/fr\/validation-donnees-nodejs\/\">Validation des Donn\u00e9es dans Node.js : 12 \u00c9tapes [2026]<\/a> &#8211; Valider les donn\u00e9es en entr\u00e9e avant d&#8217;acc\u00e9der aux ressources prot\u00e9g\u00e9es par Keycloak<\/li>\n<li><a href=\"\/fr\/tls-1-3-vs-tls-1-2\/\">TLS 1.3 vs TLS 1.2 : 40 % Plus Rapide [2026]<\/a> &#8211; Configurer TLS correctement pour chiffrer les communications entre Node.js et Keycloak<\/li>\n<li><a href=\"\/fr\/csrf-protection-nodejs\/\">Protection CSRF dans Node.js : 12 \u00c9tapes [2026]<\/a> &#8211; Comprendre les attaques CSRF que PKCE prot\u00e8ge dans le flux OAuth 2.0<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"questions-frequentes-sur-keycloak-et-node-js\">Questions fr\u00e9quentes sur Keycloak et Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quelle-est-la-difference-entre-keycloak-connect-et-keycloak-js\">Quelle est la diff\u00e9rence entre keycloak-connect et keycloak-js ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>keycloak-connect<\/code> (26.1.1, 523 125 t\u00e9l\u00e9chargements\/mois) est le middleware Express c\u00f4t\u00e9 serveur. Il prot\u00e8ge les routes Node.js, valide les tokens Bearer entrants et g\u00e8re les sessions. <code>keycloak-js<\/code> (26.2.4, 3,8 millions de t\u00e9l\u00e9chargements\/mois) est l&#8217;adaptateur JavaScript c\u00f4t\u00e9 navigateur. Il g\u00e8re le flux de redirection vers Keycloak, stocke les tokens en m\u00e9moire JavaScript et rafra\u00eechit automatiquement les tokens expir\u00e9s. Pour une API REST pure sans interface web, utilisez uniquement <code>keycloak-connect<\/code>. Pour une application full-stack avec interface utilisateur, combinez les deux.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"comment-migrer-depuis-passport-js-vers-keycloak\">Comment migrer depuis Passport.js vers Keycloak ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">La migration s&#8217;effectue en trois phases sans interruption de service. Phase 1 (semaine 1-2) : d\u00e9ployer Keycloak et importer les utilisateurs existants via l&#8217;API REST d&#8217;import en masse (<code>POST \/admin\/realms\/{realm}\/users<\/code>) ou via un provider LDAP si les utilisateurs sont dans un annuaire Active Directory. Phase 2 (semaine 3-4) : ajouter keycloak-connect en parall\u00e8le de Passport.js avec un flag d&#8217;environnement pour router une portion du trafic vers Keycloak (canary release \u00e0 10%, puis 50%, puis 100%). Phase 3 (semaine 5-6) : supprimer Passport.js une fois que 100% du trafic est valid\u00e9 sur Keycloak pendant 48 heures sans incident.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"peut-on-utiliser-keycloak-avec-next-js-app-router\">Peut-on utiliser Keycloak avec Next.js App Router ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Oui. Pour Next.js 14+ avec App Router, utilisez <code>@auth\/nextjs<\/code> (anciennement NextAuth.js v5) avec le provider Keycloak OIDC. La configuration requiert l&#8217;URL de l&#8217;issuer : <code>issuer: \"http:\/\/localhost:8080\/realms\/mon-app\"<\/code>. <code>@auth\/nextjs<\/code> g\u00e8re automatiquement PKCE, le stockage s\u00e9curis\u00e9 des tokens via les Server Components (jamais c\u00f4t\u00e9 client) et le rafra\u00eechissement des tokens. Pour les API Routes Next.js, les tokens sont accessibles c\u00f4t\u00e9 serveur via <code>auth()<\/code> sans exposition au navigateur.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"keycloak-supporte-t-il-les-architectures-microservices\">Keycloak supporte-t-il les architectures microservices ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keycloak est con\u00e7u pour les architectures microservices. Chaque service Node.js est un client Keycloak distinct avec ses propres r\u00f4les, politiques et scopes. Le token JWT propag\u00e9 entre services via le header Authorization contient les claims n\u00e9cessaires pour les d\u00e9cisions d&#8217;autorisation locales sans appel r\u00e9seau suppl\u00e9mentaire (validation locale de signature). Pour les communications service-\u00e0-service sans intervention utilisateur, utilisez le Client Credentials Grant : chaque service s&#8217;authentifie avec son propre <code>client_id<\/code> et <code>client_secret<\/code>, obtenant un token avec les scopes propres aux appels machine-to-machine. Keycloak 26.6.0 ajoute le JWT Authorization Grant (RFC 7523) comme alternative plus s\u00e9curis\u00e9e au secret client pour les architectures zero-trust.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"comment-detecter-les-tentatives-dattaque-sur-keycloak\">Comment d\u00e9tecter les tentatives d&#8217;attaque sur Keycloak ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keycloak 26.6.0 expose nativement les m\u00e9triques Prometheus sur le port 9000. La m\u00e9trique <code>keycloak_failed_login_attempts_total<\/code> signale les attaques bruteforce : configurez une alerte \u00e0 partir de 10 \u00e9checs par minute sur une m\u00eame adresse IP. La protection bruteforce int\u00e9gr\u00e9e (activ\u00e9e dans ce tutoriel avec <code>failureFactor: 5<\/code>) verrouille les comptes apr\u00e8s 5 tentatives. Int\u00e9grez les logs JSON Keycloak (activ\u00e9s avec <code>KC_LOG_FORMAT=json<\/code>) dans votre SIEM (Elastic, Graylog, Wazuh) pour corr\u00e9ler les \u00e9v\u00e9nements d&#8217;authentification \u00e9chou\u00e9e avec d&#8217;autres indicateurs de compromission (IP abuse, horaires inhabituels, g\u00e9olocalisation).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quelle-est-la-duree-de-vie-recommandee-pour-les-tokens-en-production\">Quelle est la dur\u00e9e de vie recommand\u00e9e pour les tokens en production ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Les recommandations de l&#8217;ANSSI et du SANS Institute convergent sur les valeurs suivantes pour les API expos\u00e9es sur internet : access token \u00e0 300 secondes (5 minutes), refresh token \u00e0 1 800 secondes (30 minutes d&#8217;inactivit\u00e9), session SSO maximale \u00e0 8 heures (journ\u00e9e de travail). Pour les applications manipulant des donn\u00e9es tr\u00e8s sensibles (donn\u00e9es de sant\u00e9, donn\u00e9es bancaires), r\u00e9duisez l&#8217;access token \u00e0 60 secondes et le refresh token \u00e0 900 secondes. La rotation des refresh tokens (<code>refreshTokenMaxReuse: 0<\/code>) doit \u00eatre activ\u00e9e dans tous les cas, car elle est le seul m\u00e9canisme de d\u00e9tection de rejeu disponible sans introspection.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"keycloak-connect-est-il-maintenu-activement-en-2026\">keycloak-connect est-il maintenu activement en 2026 ?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Oui. <code>keycloak-connect<\/code> est maintenu par l&#8217;\u00e9quipe officielle Red Hat\/Keycloak. La version 26.1.1, publi\u00e9e en parall\u00e8le du serveur Keycloak 26.x, est la derni\u00e8re version stable au moment de la r\u00e9daction de ce tutoriel (juin 2026). Avec 523 125 t\u00e9l\u00e9chargements mensuels et une int\u00e9gration dans le cycle de release officiel Keycloak, le package est activement maintenu. Pour les nouvelles fonctionnalit\u00e9s Keycloak 26.6 (JWT Authorization Grant, DPoP am\u00e9lior\u00e9), v\u00e9rifiez la roadmap du package sur GitHub pour l&#8217;inclusion dans une version keycloak-connect future. En attendant, ces fonctionnalit\u00e9s sont accessibles directement via l&#8217;API REST Keycloak sans passer par le middleware.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Keycloak s&#8217;impose en 2026 comme la solution open source de r\u00e9f\u00e9rence pour la gestion des identit\u00e9s et des acc\u00e8s. Avec 523 125 t\u00e9l\u00e9chargements mensuels pour le package keycloak-connect et 3,8\u2026<\/p>\n","protected":false},"author":5,"featured_media":292,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-291","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\/291","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/comments?post=291"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/291\/revisions"}],"predecessor-version":[{"id":293,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/posts\/291\/revisions\/293"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media\/292"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/media?parent=291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/categories?post=291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fr\/wp-json\/wp\/v2\/tags?post=291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}