Keycloak s’impose en 2026 comme la solution open source de référence pour la gestion des identités et des accès. Avec 523 125 téléchargements mensuels pour le package keycloak-connect et 3,8 millions pour keycloak-js, l’intégration de Keycloak dans les applications Node.js est devenue une compétence incontournable pour tout développeur soucieux de sécurité. La version 26.6.3, publiée le 4 juin 2026, apporte le JWT Authorization Grant, la fédération de clients et la prise en charge native de DPoP, renforçant un écosystème bâti sur OAuth 2.0 et OpenID Connect.
Ce tutoriel vous guide pas à pas, de l’installation de Keycloak via Docker jusqu’à la protection de vos routes Express avec rotation des refresh tokens et validation par introspection. Les CVE CVE-2026-0707 (contournement de contrôles de sécurité) et CVE-2026-2575 (déni de service SAML), corrigées dans la version 26.5.4, rappellent que configurer correctement son fournisseur d’identité n’est pas une option. En suivant ce guide, vous obtenez une API Node.js sécurisée, conforme aux normes PKCE et OIDC, prête pour la production en environnement européen.
Prérequis : versions, outils et connaissances requises
Avant de démarrer, assurez-vous de disposer des éléments suivants sur votre machine de développement. Toutes les versions listées sont celles testées dans ce tutoriel.
| Composant | Version requise | Notes |
|---|---|---|
| Node.js | 20.x LTS ou 22.x | Modules ESM supportés nativement |
| npm | 10.x ou supérieur | Inclus avec Node.js 20+ |
| Docker | 25.x ou supérieur | Pour lancer Keycloak en local |
| keycloak-connect | 26.1.1 | Middleware officiel Keycloak pour Express |
| Express | 4.19.x | Framework HTTP Node.js |
| Keycloak Server | 26.6.3 | Dernière version stable (4 juin 2026) |
| curl | 8.x | Pour tester les endpoints REST |
| openssl | 3.x | Génération des certificats TLS locaux |
Connaissances recommandées : bases de JavaScript asynchrone (async/await), notions de HTTP (codes de réponse, headers), et compréhension minimale des tokens JWT. Aucune expérience préalable avec Keycloak n’est nécessaire. Ce tutoriel part de zéro et explique chaque décision de configuration.
Architecture OAuth 2.0 et OpenID Connect avec Keycloak
Comprendre l’architecture avant d’écrire du code évite les erreurs de conception coûteuses. OAuth 2.0, défini dans la RFC 6749, est un framework d’autorisation permettant à une application tierce d’accéder à des ressources au nom d’un utilisateur. OpenID Connect (OIDC) est une couche d’identité construite sur OAuth 2.0, permettant en plus l’authentification de l’utilisateur via un ID Token signé. Ces deux standards sont supportés nativement par Keycloak, qui joue le rôle de fournisseur d’identité (IdP) centralisé.
Les quatre acteurs du flux Authorization Code
Dans un déploiement Keycloak + Node.js, quatre acteurs interagissent. L’utilisateur (Resource Owner) initie l’authentification depuis son navigateur. Le client (votre application Node.js/Express) redirige l’utilisateur vers Keycloak et consomme les tokens. Le serveur d’autorisation (Keycloak 26.6.3) valide les identifiants, applique les politiques de sécurité et émet les tokens. Le serveur de ressources (votre API Express) vérifie les access tokens Bearer sur chaque requête protégée.
Le flux complet se déroule en cinq étapes. L’application redirige l’utilisateur vers l’endpoint /authorize de Keycloak avec un code challenge PKCE. L’utilisateur s’authentifie sur la page Keycloak (avec mot de passe, WebAuthn, OTP selon la configuration). Keycloak retourne un code d’autorisation à l’URL de redirection. L’application échange ce code contre un access token, un refresh token et un ID token via l’endpoint /token. Chaque requête à l’API inclut l’access token dans le header Authorization: Bearer.
Pourquoi PKCE est obligatoire en 2026
PKCE (Proof Key for Code Exchange), défini dans la RFC 7636, est désormais obligatoire pour tous les clients publics selon les recommandations OAuth 2.1 (qui consolide les meilleures pratiques de sécurité OAuth). L’attaque visée : un attaquant intercepte le code d’autorisation retourné par Keycloak (via les logs du serveur, un malware, ou une redirection compromise) et l’échange contre des tokens avant l’application légitime. PKCE empêche cela en liant cryptographiquement le code challenge (SHA-256 du code verifier, envoyé lors de la demande d’autorisation) au code verifier (envoyé lors de l’échange). Sans la clé privée du verifier, le code intercepté est inutilisable. Keycloak 26.6.3 force PKCE par défaut pour tous les clients de type public.
Étape 1 : Installer Keycloak 26.6.3 via Docker
Docker est la méthode la plus rapide pour démarrer Keycloak en développement. L’image officielle disponible sur quay.io/keycloak/keycloak intègre une base de données H2 en mémoire adaptée aux tests locaux. Pour la production, remplacez systématiquement H2 par PostgreSQL 16 ou supérieur.
# Lancer Keycloak 26.6.3 en mode développement
docker run -d \
--name keycloak-dev \
-p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin_secret_2026 \
quay.io/keycloak/keycloak:26.6.3 \
start-dev
# Vérifier que le conteneur est démarré et attendre la disponibilité
docker logs -f keycloak-dev 2>&1 | grep -m1 "Keycloak.*started"
# Tester la disponibilité de l'endpoint de santé
curl -s http://localhost:8080/health/ready | python3 -m json.tool
La console d’administration est accessible sur http://localhost:8080/admin. Le démarrage complet prend entre 15 et 40 secondes selon votre machine. Le flag start-dev désactive les optimisations de production : TLS obligatoire, base H2 en mémoire (données perdues au redémarrage), et endpoints de diagnostic activés. Ne l’utilisez jamais en production.
Pour un environnement avec persistance des données et PostgreSQL, utilisez Docker Compose :
# docker-compose.yml pour Keycloak 26.6.3 + PostgreSQL 16
version: '3.9'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: kc_db_password_2026
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5
networks:
- keycloak-net
keycloak:
image: quay.io/keycloak/keycloak:26.6.3
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: kc_db_password_2026
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin_secret_2026
KC_HOSTNAME: localhost
KC_METRICS_ENABLED: "true"
KC_LOG_LEVEL: INFO
KC_LOG_FORMAT: json
ports:
- "8080:8080"
- "9000:9000"
depends_on:
postgres:
condition: service_healthy
networks:
- keycloak-net
volumes:
postgres_data:
networks:
keycloak-net:
Le port 9000 expose les métriques Prometheus et l’endpoint de santé (/health/ready, /metrics). L’activation des métriques (KC_METRICS_ENABLED=true) et des logs structurés JSON (KC_LOG_FORMAT=json) sont des prérequis pour intégrer Keycloak dans un stack d’observabilité (Grafana, ELK, Loki).
Étape 2 : Créer le Realm et le Client dans Keycloak
Un Realm dans Keycloak est un espace d’isolation complet qui regroupe utilisateurs, clients, rôles, fédérations d’identité et politiques de sécurité. L’interface d’administration permet de tout configurer via l’UI, mais l’API REST de Keycloak offre une approche reproductible et automatisable en CI/CD. Créer votre realm via l’API garantit l’idempotence des déploiements.
Créer le Realm et le Client via l’API REST
# 1. Obtenir un token admin depuis le realm master
TOKEN=$(curl -s -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=admin-cli' \
-d 'username=admin' \
-d 'password=admin_secret_2026' \
-d 'grant_type=password' | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
# 2. Créer le realm "mon-app" avec durées de vie de tokens optimisées
curl -s -X POST http://localhost:8080/admin/realms \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"realm": "mon-app",
"enabled": true,
"displayName": "Mon Application",
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"accessTokenLifespan": 300,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"refreshTokenMaxReuse": 0,
"bruteForceProtected": true,
"failureFactor": 5,
"waitIncrementSeconds": 60
}'
# 3. Créer un client confidentiel pour l'API Node.js
curl -s -X POST http://localhost:8080/admin/realms/mon-app/clients \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"clientId": "nodejs-api",
"name": "API Node.js",
"protocol": "openid-connect",
"publicClient": false,
"authorizationServicesEnabled": false,
"serviceAccountsEnabled": true,
"directAccessGrantsEnabled": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"redirectUris": ["http://localhost:3000/callback", "http://localhost:3000/*"],
"webOrigins": ["http://localhost:3000"],
"attributes": {
"pkce.code.challenge.method": "S256"
}
}'
echo "Realm et client créés avec succès"
Paramètre critique : "refreshTokenMaxReuse": 0 active la rotation obligatoire des refresh tokens. Chaque utilisation d’un refresh token génère un nouveau token et invalide immédiatement l’ancien. Si Keycloak détecte qu’un refresh token déjà utilisé est présenté à nouveau (signe possible de vol), il révoque toute la famille de tokens de la session concernée. La durée de vie de l’access token est fixée à 300 secondes (5 minutes), valeur recommandée par l’ANSSI pour les API exposées sur internet. Le paramètre "bruteForceProtected": true verrouille les comptes après 5 tentatives échouées, avec un délai d’attente croissant de 60 secondes.
Étape 3 : Créer les utilisateurs et les rôles
Les rôles Keycloak structurent les autorisations à deux niveaux : les rôles de realm (globaux à tous les clients) et les rôles de client (spécifiques à une application). Pour une API REST, définissez vos rôles métier comme des rôles de realm. Cela simplifie la gestion des autorisations dans une architecture microservices où plusieurs clients partagent les mêmes rôles.
# Créer un utilisateur de test avec mot de passe permanent
curl -s -X POST http://localhost:8080/admin/realms/mon-app/users \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"username": "alice",
"email": "[email protected]",
"enabled": true,
"emailVerified": true,
"firstName": "Alice",
"lastName": "Dupont",
"credentials": [{
"type": "password",
"value": "MotDePasseTest2026!",
"temporary": false
}]
}'
# Créer les rôles métier du realm
for ROLE in "lecteur" "editeur" "admin-api"; do
curl -s -X POST http://localhost:8080/admin/realms/mon-app/roles \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"name\": \"$ROLE\", \"description\": \"Rôle $ROLE pour l'API Node.js\"}"
done
echo "Utilisateur et rôles créés"
La gestion des rôles dans Keycloak suit le modèle RBAC (Role-Based Access Control). Dans votre API Node.js, vous vérifierez la présence de ces rôles dans le champ realm_access.roles du JWT décodé. Keycloak inclut automatiquement ces claims dans les access tokens. Pour les rôles spécifiques à un client, ils apparaissent dans resource_access.nodejs-api.roles. Choisissez des rôles de realm pour des autorisations partagées entre applications, et des rôles de client pour des autorisations propres à une seule application.
Étape 4 : Initialiser le projet Node.js
Créez la structure de votre projet avant d’installer les dépendances. Une organisation claire facilite la maintenance et la revue de code dans les équipes.
# Initialiser le projet Node.js
mkdir keycloak-nodejs-api && cd keycloak-nodejs-api
npm init -y
# Installer les dépendances de production
npm install [email protected] [email protected] [email protected] [email protected]
# Installer les outils de développement
npm install --save-dev [email protected] [email protected] [email protected]
# Créer la structure du projet
mkdir -p src/{routes,middleware,config}
touch src/server.js \
src/config/keycloak.js \
src/middleware/auth.js \
src/middleware/pkce.js \
src/routes/public.js \
src/routes/protected.js \
src/routes/auth.js \
.env .env.example .gitignore
# Ajouter .env au .gitignore
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
Versions importantes à retenir : [email protected] est la dernière version stable alignée sur le serveur Keycloak 26.x. La version 26.1.1 résout un problème de validation de nonce OIDC présent dans les versions 26.0.x. Utilisez toujours une version de keycloak-connect dont le numéro de version majeur correspond à celui de votre serveur Keycloak. Une version de middleware 25.x avec un serveur 26.x peut générer des erreurs de décodage de configuration imprévues.
Fichier .env à configurer, jamais à committer dans votre dépôt :
# .env - JAMAIS COMMITTER CE FICHIER (ajoutez .env dans .gitignore)
KEYCLOAK_BASE_URL=http://localhost:8080
KEYCLOAK_REALM=mon-app
KEYCLOAK_CLIENT_ID=nodejs-api
KEYCLOAK_CLIENT_SECRET=votre_secret_client_keycloak_ici
SESSION_SECRET=une_chaine_aleatoire_de_64_caracteres_minimum_generee_avec_openssl_rand_hex_32
PORT=3000
NODE_ENV=development
Étape 5 : Configurer le middleware keycloak-connect
keycloak-connect est le middleware officiel Keycloak pour Express. Il gère automatiquement la validation des tokens Bearer (signature cryptographique, expiration, issuer), la gestion des sessions et le rafraîchissement transparent des access tokens expirés. Contrairement à une validation JWT statique manuelle, il peut également effectuer une vérification de révocation via l’endpoint d’introspection.
// src/config/keycloak.js
'use strict';
require('dotenv').config();
const Keycloak = require('keycloak-connect');
let _keycloak;
const keycloakConfig = {
realm: process.env.KEYCLOAK_REALM,
'auth-server-url': process.env.KEYCLOAK_BASE_URL + '/',
'ssl-required': process.env.NODE_ENV === 'production' ? 'all' : 'external',
resource: process.env.KEYCLOAK_CLIENT_ID,
credentials: {
secret: process.env.KEYCLOAK_CLIENT_SECRET
},
'confidential-port': 0,
'bearer-only': true // Mode API REST : pas de redirection vers la page de login
};
function initKeycloak(memoryStore) {
if (_keycloak) {
console.warn('[KEYCLOAK] Instance déjà initialisée, retour de l\'instance existante');
return _keycloak;
}
console.log(`[KEYCLOAK] Connexion au realm "${keycloakConfig.realm}" sur ${process.env.KEYCLOAK_BASE_URL}`);
_keycloak = new Keycloak({ store: memoryStore }, keycloakConfig);
return _keycloak;
}
function getKeycloak() {
if (!_keycloak) {
throw new Error('Keycloak non initialisé. Appelez initKeycloak() d\'abord.');
}
return _keycloak;
}
module.exports = { initKeycloak, getKeycloak };
Le paramètre 'bearer-only': true est fondamental pour une API REST. Sans ce paramètre, keycloak-connect tente de rediriger les requêtes non authentifiées vers la page de login Keycloak, comportement inadapté pour une API consommée par des clients programmatiques (applications mobiles, frontends React/Vue, autres microservices). En mode bearer-only, les requêtes sans token Bearer valide reçoivent directement une réponse 401 Unauthorized avec le header WWW-Authenticate: Bearer realm="mon-app". Seul le paramètre 'auth-server-url' doit se terminer par un slash.
Étape 6 : Construire le serveur Express principal
// src/server.js
'use strict';
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const { initKeycloak } = require('./config/keycloak');
const publicRoutes = require('./routes/public');
const protectedRoutes = require('./routes/protected');
const authRoutes = require('./routes/auth');
const app = express();
const PORT = parseInt(process.env.PORT || '3000', 10);
// Store de sessions (remplacer par Redis en production)
const memoryStore = new session.MemoryStore();
// Configuration des sessions (requise par keycloak-connect)
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: memoryStore,
name: 'kc.session', // Nom de cookie non générique
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true, // Protection XSS
sameSite: 'lax', // Protection CSRF partielle
maxAge: 30 * 60 * 1000 // 30 minutes
}
}));
// Middleware Keycloak (doit être après session)
const keycloak = initKeycloak(memoryStore);
app.use(keycloak.middleware({
logout: '/logout',
admin: '/'
}));
// Parsing JSON avec limite de taille
app.use(express.json({ limit: '1mb' }));
// En-têtes de sécurité de base
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '0'); // Désactivé en faveur de CSP
next();
});
// Routes
app.use('/auth', authRoutes);
app.use('/api/public', publicRoutes);
app.use('/api/protected', protectedRoutes(keycloak));
// Gestion d'erreurs globale (doit être en dernier)
app.use((err, req, res, next) => {
const status = err.status || 500;
console.error(`[ERROR] ${err.message} (${status})`);
res.status(status).json({
error: err.message || 'Erreur interne du serveur',
code: err.code || 'INTERNAL_ERROR'
});
});
const server = app.listen(PORT, () => {
console.log(`[SERVER] Démarré sur le port ${PORT} (${process.env.NODE_ENV})`);
console.log(`[SERVER] Realm Keycloak: ${process.env.KEYCLOAK_REALM}`);
});
module.exports = { app, server };
Étape 7 : Protéger les routes avec le middleware d’authentification
keycloak-connect expose plusieurs méthodes de protection des routes. keycloak.protect() exige un token Bearer valide. keycloak.protect('realm:role') exige un token valide et le rôle de realm spécifié. keycloak.protect('resource:scope') applique les politiques de ressources Keycloak pour des contrôles d’accès fins (fine-grained authorization).
// src/routes/protected.js
'use strict';
const express = require('express');
module.exports = function(keycloak) {
const router = express.Router();
// Accessible à tout utilisateur authentifié
router.get('/profil',
keycloak.protect(),
(req, res) => {
const tokenContent = req.kauth.grant.access_token.content;
res.json({
sub: tokenContent.sub,
email: tokenContent.email,
nom: tokenContent.name,
prenom: tokenContent.given_name,
roles: tokenContent.realm_access?.roles || [],
sessionState: tokenContent.session_state,
expiresAt: new Date(tokenContent.exp * 1000).toISOString()
});
}
);
// Accessible uniquement aux éditeurs
router.get('/articles',
keycloak.protect('realm:editeur'),
(req, res) => {
res.json({
articles: [
{ id: 1, titre: 'Sécurité OAuth 2.0 en 2026', statut: 'publié' },
{ id: 2, titre: 'DPoP et rotation de tokens', statut: 'brouillon' }
]
});
}
);
// Route administrative avec vérification de rôle personnalisée
router.delete('/articles/:id',
keycloak.protect(),
(req, res, next) => {
const roles = req.kauth.grant.access_token.content.realm_access?.roles || [];
if (!roles.includes('admin-api')) {
return res.status(403).json({
error: 'Accès interdit',
code: 'INSUFFICIENT_ROLE',
requis: 'admin-api',
present: roles
});
}
next();
},
(req, res) => {
res.json({
supprime: true,
id: parseInt(req.params.id, 10),
par: req.kauth.grant.access_token.content.email
});
}
);
// Route avec informations du token pour le débogage (désactiver en production)
if (process.env.NODE_ENV !== 'production') {
router.get('/debug/token',
keycloak.protect(),
(req, res) => {
res.json(req.kauth.grant.access_token.content);
}
);
}
return router;
};
L’objet req.kauth.grant injecté par keycloak-connect donne accès à l’intégralité du grant OAuth 2.0. Le champ access_token.content contient le JWT décodé avec les claims standard OIDC (sub, email, name, iat, exp) et les claims Keycloak spécifiques (realm_access, resource_access, session_state, azp). La route /debug/token est utile pendant le développement pour inspecter le contenu exact du token, mais doit être désactivée en production car elle expose des métadonnées de session.
Étape 8 : Implémenter PKCE pour le flux Authorization Code
Pour les applications web avec une interface utilisateur, vous devez implémenter le flux Authorization Code complet avec PKCE. La mise en oeuvre suit la RFC 7636 à la lettre, en utilisant uniquement le module crypto natif de Node.js, sans dépendances tierces supplémentaires.
// src/middleware/pkce.js
'use strict';
const crypto = require('crypto');
// Générer un code verifier de 43 à 128 caractères (base64url, RFC 7636 section 4.1)
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
// Dériver le code challenge avec SHA-256 (méthode S256, RFC 7636 section 4.2)
function generateCodeChallenge(verifier) {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
// Middleware : démarrer le flux OAuth 2.0 + PKCE
function startAuthFlow(req, res) {
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);
const state = crypto.randomBytes(16).toString('hex');
// Stocker en session côté serveur uniquement (jamais exposé au client)
req.session.pkce_verifier = verifier;
req.session.oauth_state = state;
req.session.pkce_created_at = Date.now();
const params = new URLSearchParams({
client_id: process.env.KEYCLOAK_CLIENT_ID,
redirect_uri: `http://localhost:${process.env.PORT}/auth/callback`,
response_type: 'code',
scope: 'openid email profile',
state: state,
code_challenge: challenge,
code_challenge_method: 'S256'
});
const authUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth?${params}`;
res.redirect(authUrl);
}
// Échanger le code d'autorisation contre des tokens
async function exchangeCode(code, verifier) {
const tokenUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`;
const response = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.KEYCLOAK_CLIENT_ID,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
code: code,
redirect_uri: `http://localhost:${process.env.PORT}/auth/callback`,
code_verifier: verifier
})
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw Object.assign(
new Error(err.error_description || err.error || 'Échec de l\'échange de tokens'),
{ status: response.status, code: err.error }
);
}
return response.json();
}
module.exports = { startAuthFlow, exchangeCode };
Le paramètre state anti-CSRF est obligatoire. Si le state reçu dans le callback ne correspond pas au state stocké en session, rejeter la requête immédiatement. Un state manquant ou incorrect indique une tentative d’attaque CSRF (un site malveillant force l’utilisateur à initier un flux OAuth vers votre application) ou une confusion de flux. Vérifiez aussi que le challenge PKCE n’a pas plus de 10 minutes (pkce_created_at) pour limiter la fenêtre d’attaque.
Étape 9 : Traiter le callback et gérer les tokens
// src/routes/auth.js
'use strict';
const express = require('express');
const { startAuthFlow, exchangeCode } = require('../middleware/pkce');
const router = express.Router();
// Démarrer le flux OAuth 2.0
router.get('/login', startAuthFlow);
// Traiter le callback Keycloak
router.get('/callback', async (req, res) => {
const { code, state, error, error_description } = req.query;
// Vérification anti-CSRF du state (obligatoire)
if (!state || state !== req.session.oauth_state) {
return res.status(400).json({
error: 'state_mismatch',
description: 'Paramètre state invalide ou absent'
});
}
// Vérifier que le challenge PKCE n'est pas expiré (10 minutes max)
const pkceAge = Date.now() - (req.session.pkce_created_at || 0);
if (pkceAge > 10 * 60 * 1000) {
req.session.destroy();
return res.status(400).json({ error: 'pkce_expired', action: 'relogin' });
}
if (error) {
return res.status(400).json({ error, description: error_description });
}
if (!code) {
return res.status(400).json({ error: 'missing_code' });
}
try {
const tokens = await exchangeCode(code, req.session.pkce_verifier);
// Nettoyer les données PKCE de la session immédiatement après utilisation
delete req.session.pkce_verifier;
delete req.session.oauth_state;
delete req.session.pkce_created_at;
// Stocker les tokens côté serveur (jamais envoyés au client JS)
req.session.access_token = tokens.access_token;
req.session.refresh_token = tokens.refresh_token;
req.session.id_token = tokens.id_token;
req.session.token_expires_at = Date.now() + (tokens.expires_in * 1000);
res.json({
message: 'Authentification réussie',
expires_in: tokens.expires_in,
token_type: tokens.token_type,
scope: tokens.scope
});
} catch (err) {
console.error('[AUTH CALLBACK]', err.message);
res.status(err.status || 401).json({
error: err.code || 'auth_failed',
description: err.message
});
}
});
// Rafraîchir les tokens (rotation activée)
router.post('/refresh', async (req, res) => {
const refreshToken = req.session.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'no_refresh_token', action: 'relogin' });
}
const tokenUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`;
try {
const response = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.KEYCLOAK_CLIENT_ID,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
refresh_token: refreshToken
})
});
if (!response.ok) {
// Refresh token révoqué ou expiré : invalider la session
req.session.destroy();
return res.status(401).json({ error: 'refresh_invalid', action: 'relogin' });
}
const tokens = await response.json();
// Remplacer TOUS les tokens (rotation)
req.session.access_token = tokens.access_token;
req.session.refresh_token = tokens.refresh_token; // Nouveau refresh token (rotation)
req.session.token_expires_at = Date.now() + (tokens.expires_in * 1000);
res.json({
message: 'Tokens rafraîchis',
expires_in: tokens.expires_in
});
} catch (err) {
console.error('[REFRESH ERROR]', err.message);
res.status(503).json({ error: 'refresh_unavailable' });
}
});
// Déconnexion avec révocation
router.post('/logout', async (req, res) => {
const idToken = req.session.id_token;
if (idToken) {
const logoutUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/logout`;
await fetch(logoutUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.KEYCLOAK_CLIENT_ID,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
id_token_hint: idToken
})
}).catch(err => console.error('[LOGOUT ERROR]', err.message));
}
req.session.destroy();
res.json({ message: 'Déconnexion réussie' });
});
module.exports = router;
Étape 10 : Validation par introspection et révocation des tokens
La validation statique d’un JWT (vérification de la signature et de l’expiration localement) ne suffit pas dans un scénario de révocation. Si un administrateur révoque manuellement les sessions d’un utilisateur dans Keycloak, son access token reste valide localement jusqu’à son expiration naturelle (300 secondes). Pour les actions sensibles, l’introspection en temps réel est indispensable.
// src/middleware/auth.js
'use strict';
// Middleware d'introspection temps réel (pour les routes critiques)
async function introspectToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: 'missing_token',
description: 'Header Authorization manquant ou malformé. Format attendu: Bearer '
});
}
const token = authHeader.slice(7); // Retirer "Bearer "
const introspectUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token/introspect`;
try {
const response = await fetch(introspectUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// Authentification client via Basic Auth (client_id:client_secret)
'Authorization': 'Basic ' + Buffer.from(
`${process.env.KEYCLOAK_CLIENT_ID}:${process.env.KEYCLOAK_CLIENT_SECRET}`
).toString('base64')
},
body: new URLSearchParams({ token }),
signal: AbortSignal.timeout(5000) // Timeout 5 secondes
});
if (!response.ok) {
throw new Error(`Keycloak introspection HTTP ${response.status}`);
}
const data = await response.json();
if (!data.active) {
return res.status(401).json({
error: 'token_inactive',
description: 'Token révoqué, expiré ou invalide'
});
}
// Attacher les claims vérifiés à la requête
req.tokenClaims = data;
next();
} catch (err) {
if (err.name === 'TimeoutError') {
console.error('[INTROSPECTION TIMEOUT]');
return res.status(503).json({ error: 'auth_service_timeout' });
}
// Fail-secure : en cas d'erreur, refuser par défaut
console.error('[INTROSPECTION ERROR]', err.message);
res.status(503).json({
error: 'auth_service_unavailable',
description: 'Service d\'authentification temporairement indisponible'
});
}
}
module.exports = { introspectToken };
La stratégie fail-secure dans le bloc catch est une décision de conception délibérée : si Keycloak est inaccessible, le middleware refuse toutes les requêtes plutôt que de les autoriser. Cette approche est recommandée pour les APIs manipulant des données sensibles (données personnelles, paiements, actions irréversibles). Pour les APIs à haute disponibilité où la disponibilité prime, implémentez un fallback sur validation locale de signature JWT avec vérification des JWKS, mais uniquement pour les tokens récents (moins de 60 secondes) qui ne peuvent pas encore avoir été révoqués.
Étape 11 : Sécurisation CORS et en-têtes de sécurité
Une API Keycloak mal configurée côté CORS peut exposer vos tokens à des sites malveillants. La configuration CORS doit être restrictive par défaut et ouverte uniquement aux origines légitimes. Ne configurez jamais Access-Control-Allow-Origin: * avec Access-Control-Allow-Credentials: true, combinaison interdite par les navigateurs modernes et dangereuse pour la sécurité des tokens.
// src/middleware/security.js
'use strict';
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000')
.split(',')
.map(o => o.trim());
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Request-ID');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
// En-têtes de sécurité HTTP
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
}
// Logger sécurisé : jamais de tokens ou de données sensibles dans les logs
function secureLogger(req, res, next) {
const start = process.hrtime.bigint();
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1e6;
// Format: [timestamp] METHOD path status duration requestId
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} ${res.statusCode} ${duration.toFixed(1)}ms req=${requestId}`);
// JAMAIS logger: req.headers.authorization, req.body.password, tokens
});
next();
}
module.exports = { corsMiddleware, secureLogger };
Étape 12 : Tester l’intégration avec curl et Jest
Avant de déployer, validez l’intégration complète : authentification réussie, accès aux routes protégées, refus des accès non autorisés, rafraîchissement de tokens et révocation. Ces tests couvrent les chemins critiques de votre API.
# Tests d'intégration via curl
# 1. Obtenir un token via Client Credentials Grant (service-to-service)
TOKEN_RESPONSE=$(curl -s -X POST \
"${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "client_id=${KEYCLOAK_CLIENT_ID}" \
-d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \
-d 'grant_type=client_credentials' \
-d 'scope=openid')
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token', 'ERREUR'))")
echo "Access token: ${ACCESS_TOKEN:0:60}..."
echo "Expire dans: $(echo $TOKEN_RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin).get('expires_in', 0))") secondes"
# 2. Accéder à une route protégée (doit retourner 200)
echo "\n--- Test route protégée ---"
curl -s -X GET http://localhost:3000/api/protected/profil \
-H "Authorization: Bearer $ACCESS_TOKEN" | python3 -m json.tool
# 3. Accès sans token (doit retourner 401)
echo "\n--- Test sans token (doit être 401) ---"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/protected/profil)
echo "Status: $STATUS (attendu: 401)"
# 4. Token invalide (doit retourner 401)
echo "\n--- Test token invalide (doit être 401) ---"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/protected/profil \
-H "Authorization: Bearer token_invalide_test")
echo "Status: $STATUS (attendu: 401)"
# 5. Introspection directe
echo "\n--- Introspection du token ---"
curl -s -X POST \
"${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token/introspect" \
-u "${KEYCLOAK_CLIENT_ID}:${KEYCLOAK_CLIENT_SECRET}" \
-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\")}')"
Sortie attendue pour l’introspection d’un token valide :
{
"active": true,
"sub": "nodejs-api",
"client_id": "nodejs-api",
"username": "service-account-nodejs-api",
"token_type": "Bearer",
"scope": "openid email profile",
"exp": 1750416300,
"iat": 1750416000,
"iss": "http://localhost:8080/realms/mon-app",
"jti": "5a3b2c1d-0e9f-8a7b-6c5d-4e3f2a1b0c9d",
"realm_access": {
"roles": ["offline_access", "uma_authorization", "default-roles-mon-app"]
}
}
5 pièges courants et comment les éviter
Piège 1 : Stocker le refresh token côté client JavaScript
Stocker le refresh token dans le localStorage ou un cookie sans l’attribut httpOnly expose l’utilisateur aux attaques XSS. Un seul script malveillant injecté dans votre page suffit à voler le refresh token et maintenir un accès permanent au compte, même après expiration de l’access token. La solution est catégorique : les refresh tokens ne doivent jamais quitter le serveur. Stockez-les uniquement dans la session côté serveur (objet req.session de Express). Le client ne reçoit que l’access token, dont la durée de vie limitée à 300 secondes réduit drastiquement l’impact en cas de vol.
Piège 2 : Négliger la vérification de l’Issuer du JWT
Un token JWT cryptographiquement valide peut provenir d’un realm Keycloak différent du vôtre. Si vous acceptez des tokens sans vérifier que le champ iss (issuer) correspond exactement à http://keycloak-host/realms/mon-app, vous devenez vulnérable aux attaques de confusion d’issuer : un attaquant crée son propre realm Keycloak avec les mêmes clés et génère des tokens acceptés par votre API. keycloak-connect effectue cette vérification automatiquement grâce à la configuration du realm. Si vous implémentez une validation JWT manuelle avec jsonwebtoken, le paramètre issuer de la fonction verify() est obligatoire.
Piège 3 : Utiliser start-dev en environnement de staging ou production
Le mode start-dev de Keycloak désactive TLS (toutes les communications en HTTP clair), utilise une base H2 en mémoire (toutes les données effacées au redémarrage), active des endpoints de diagnostic et des caches minimaux non adaptés à la charge. Plusieurs équipes ont déployé en staging avec start-dev et découvert des pertes de configuration entières au redémarrage de Kubernetes. En production et staging, utilisez start avec les variables KC_HOSTNAME, KC_DB, KC_HTTPS_CERTIFICATE_FILE et KC_HTTPS_CERTIFICATE_KEY_FILE correctement configurées. La commande de build optimisée (build) avant start réduit le temps de démarrage de 60 à 80%.
Piège 4 : Ignorer la révocation en temps réel pour les actions critiques
Valider un JWT localement (vérification de signature + expiration en mémoire) ne détecte pas la révocation. Si un administrateur désactive un compte utilisateur dans Keycloak à 14h00, l’access token de cet utilisateur reste valide localement jusqu’à 14h05 (300 secondes). Pour les routes manipulant des données sensibles (suppression de compte, transfert d’argent, modification d’autorisations), l’introspection en temps réel via l’endpoint Keycloak est obligatoire. Le surcoût de latence est d’environ 5 à 20 millisecondes selon la localisation de Keycloak, acceptable pour les opérations critiques. Pour les routes en lecture seule à faible risque, la validation locale est suffisante.
Piège 5 : Loguer les access tokens dans les fichiers de logs applicatifs
Les access tokens JWT contiennent des claims sensibles (email, rôles, identifiants uniques) et donnent accès à toutes les ressources protégées pendant leur durée de vie. Plusieurs incidents de sécurité documentés ont été causés par des tokens entiers loggués dans des systèmes d’agrégation de logs comme ELK ou Splunk, accessibles à de nombreux opérateurs. La règle : ne loguer jamais req.headers.authorization en entier. Si vous devez identifier un token dans les logs pour débogage, loggez uniquement les 12 premiers caractères (token.substring(0, 12) + '...') ou le champ jti (identifiant unique du token, sans valeur d’accès). N’incluez jamais les refresh tokens ni les secrets client dans aucun log.
Guide de dépannage : 10 problèmes et leurs solutions
| Erreur | Cause probable | Solution |
|---|---|---|
401 Unauthorized: token_not_provided | Header Authorization absent ou format incorrect | Vérifier le format exact : Authorization: Bearer <token> (sensible aux espaces) |
401: Token is not active | Token expiré (après 300 secondes) ou révoqué par admin | Appeler POST /auth/refresh, sinon rediriger vers le login |
403: Access denied | Rôle requis absent du token | Assigner le rôle à l’utilisateur dans l’admin Keycloak, les tokens actuels ne seront pas mis à jour immédiatement (attendre expiration) |
ECONNREFUSED localhost:8080 | Keycloak non démarré ou port erroné | Vérifier docker ps, attendre Keycloak started dans les logs, vérifier KEYCLOAK_BASE_URL |
CORS: No Access-Control-Allow-Origin | Origine non listée dans ALLOWED_ORIGINS ou Web Origins du client | Ajouter l’URL complète (avec port) dans ALLOWED_ORIGINS et dans Web Origins du client Keycloak |
invalid_grant: Code not valid | Code d’autorisation déjà échangé ou expiré (60 secondes par défaut) | Vérifier les rechargements de page sur /callback, éviter de traiter le callback deux fois |
invalid_grant: Invalid refresh token | Refresh token révoqué (rotation) ou session SSO expirée | Détruire la session (req.session.destroy()) et relancer l’authentification |
SSL_ERROR_RX_RECORD_TOO_LONG | Application tente HTTPS mais Keycloak répond en HTTP | Vérifier que KEYCLOAK_BASE_URL commence par http:// en développement |
Session not found après redémarrage | MemoryStore vide après redémarrage du processus Node.js | Utiliser connect-redis en production : npm install connect-redis redis |
Realm does not exist | Nom de realm incorrect (sensible à la casse) | Vérifier KEYCLOAK_REALM dans .env. Tester avec : curl http://localhost:8080/realms/mon-app/.well-known/openid-configuration |
Conseils avancés pour la production
Persister les sessions avec Redis. Le MemoryStore perd toutes les sessions au redémarrage et ne scale pas horizontalement entre plusieurs instances Node.js. En production, installez connect-redis : npm install connect-redis redis. Configurez new RedisStore({ client: redisClient, prefix: 'kc:', ttl: 1800 }). Redis assure la persistance des sessions entre les redémarrages et permet le scale horizontal avec une architecture multi-instances derrière un load balancer.
Mettre en cache les clés publiques JWKS. Keycloak expose ses clés publiques de signature sur l’endpoint JWKS : /realms/{realm}/protocol/openid-connect/certs. Pour la validation locale des signatures JWT (sans introspection), téléchargez ces clés au démarrage et rafraîchissez-les toutes les 24 heures ou immédiatement si une validation de signature échoue (pour gérer la rotation des clés Keycloak). Le package jwks-rsa automatise ce mécanisme avec un cache LRU configurable.
Implémenter un circuit breaker sur les appels Keycloak. Si Keycloak est indisponible pendant 30 secondes, vos routes protégées retournent 503 en cascade. Implémentez un circuit breaker avec le package opossum pour distinguer les pannes transitoires (circuit fermé, retries exponentiels) des pannes prolongées (circuit ouvert, fail-fast immédiat). Configurez 5 échecs consécutifs pour ouvrir le circuit avec un délai de récupération de 30 secondes. Cela réduit la charge sur un Keycloak en difficulté pendant sa récupération.
Surveiller les métriques Keycloak avec Prometheus. Keycloak 26.6.0 supporte nativement OpenTelemetry. Exposez les métriques en activant KC_METRICS_ENABLED=true. Les alertes prioritaires à configurer : keycloak_failed_login_attempts_total (seuil : 10 par minute par IP, alerte : attaque bruteforce), keycloak_logins_total (chute soudaine indiquant une panne), et keycloak_client_login_attempts_total (pic anormal indiquant un client compromis). Intégrez ces métriques dans Grafana avec les datasources Prometheus pour une observabilité complète.
Configurer le backchannel logout OIDC. Le backchannel logout permet à Keycloak de notifier votre API Node.js quand un utilisateur se déconnecte depuis n’importe quelle application du realm. Configurez Backchannel Logout URL: http://votre-api.fr/auth/backchannel-logout dans les paramètres Advanced de votre client Keycloak. Votre endpoint reçoit un logout_token JWT signé contenant le sid (session ID) à invalider. Cela garantit la déconnexion synchronisée entre toutes les applications du même SSO.
Séparer les clients Keycloak par environnement. Créez un client distinct pour chaque environnement (développement, staging, production). Les redirectURIs, secrets et politiques de sécurité sont différents par environnement. Un secret de développement compromis (présent dans des logs CI/CD ou un dépôt Git mal configuré) n’affecte pas la production. En production, désactivez le flux Resource Owner Password Credentials ("directAccessGrantsEnabled": false) qui expose les mots de passe utilisateurs à l’application cliente.
Comparatif des stratégies d’authentification Node.js en 2026
| Stratégie | Complexité setup | Scalabilité | Révocation temps réel | SSO multi-apps | Coût |
|---|---|---|---|---|---|
| Keycloak + keycloak-connect | Moyenne | Excellente | Oui (introspection) | Oui, natif | Gratuit (open source) |
| JWT stateless seul | Faible | Excellente | Non (stateless) | Non | Gratuit |
| Auth0 + express-jwt | Faible | Excellente | Oui | Oui | 23 €/mois/1 000 MAU |
| Sessions Express seules | Faible | Limitée (sticky) | Oui | Non | Gratuit |
| Passport.js local | Faible | Moyenne | Partielle | Non | Gratuit |
| Supabase Auth | Très faible | Excellente | Oui | Non | 0 à 25 €/mois |
Conformité RGPD et souveraineté des données en Europe
Keycloak est particulièrement adapté aux organisations européennes soumises au RGPD. Contrairement aux solutions SaaS comme Auth0 (Okta, États-Unis) ou Firebase Auth (Google, États-Unis) qui stockent les données d’identité sur des serveurs américains, Keycloak peut être hébergé intégralement dans des datacenters européens, y compris sur des infrastructures certifiées SecNumCloud pour les entités soumises aux exigences de la cybersécurité nationale française (OIV, OSE, administrations publiques).
Keycloak supporte nativement les fonctionnalités requises par le RGPD. La suppression complète des données utilisateur répond au droit à l’effacement (article 17 RGPD) : un appel API supprime toutes les sessions, tokens, attributs et l’historique d’un utilisateur. L’export des données personnelles répond au droit à la portabilité (article 20). Les journaux d’audit des connexions et des actions administratives, conservés pendant une durée configurable, répondent aux obligations de traçabilité. Activez l’audit dans Realm Settings, onglet Events, en cochant “Save Events” avec une rétention de 30 jours minimum.
La CVE-2026-0707 (contournement de contrôles de sécurité via manipulation du header Authorization, corrigée dans 26.5.4) et la CVE-2026-2575 (déni de service par décompression excessive de requêtes SAML, corrigée dans 26.5.4) illustrent l’importance d’une maintenance active. Keycloak publie 4 versions mineures par an selon un calendrier public. Abonnez-vous aux annonces de sécurité sur le blog officiel Keycloak et configurez un pipeline de mise à jour automatique en environnement de staging pour valider chaque nouvelle version avant déploiement en production.
Ressources et références officielles
Pour approfondir l’intégration Keycloak et les standards OAuth 2.0/OIDC, ces ressources font autorité. La documentation officielle Keycloak couvre l’ensemble des fonctionnalités avec des guides par cas d’usage. La spécification OpenID Connect détaille le protocole de couche d’identité. La spécification OIDC Core 1.0 décrit les flows et les claims. Le dépôt GitHub de Keycloak (plus de 23 000 étoiles) est la référence pour les issues connues et les contributions. L’explication PKCE sur oauth.net vulgarise la RFC 7636 avec des schémas.
Couverture connexe
Articles liés sur shattered.io
- JWT Authentication dans Node.js : 10 Étapes [2026] – Comprendre la validation statique des JWT avant d’aborder l’introspection Keycloak
- Gestion des Sessions Node.js : 11 Étapes [2026] – Configuration des stores de sessions persistants pour keycloak-connect en production
- En-têtes de Sécurité HTTP dans Node.js : 12 Étapes [2026] – Compléter la sécurisation de votre API Express avec des headers HTTP appropriés
- Validation des Données dans Node.js : 12 Étapes [2026] – Valider les données en entrée avant d’accéder aux ressources protégées par Keycloak
- TLS 1.3 vs TLS 1.2 : 40 % Plus Rapide [2026] – Configurer TLS correctement pour chiffrer les communications entre Node.js et Keycloak
- Protection CSRF dans Node.js : 12 Étapes [2026] – Comprendre les attaques CSRF que PKCE protège dans le flux OAuth 2.0
Questions fréquentes sur Keycloak et Node.js
Quelle est la différence entre keycloak-connect et keycloak-js ?
keycloak-connect (26.1.1, 523 125 téléchargements/mois) est le middleware Express côté serveur. Il protège les routes Node.js, valide les tokens Bearer entrants et gère les sessions. keycloak-js (26.2.4, 3,8 millions de téléchargements/mois) est l’adaptateur JavaScript côté navigateur. Il gère le flux de redirection vers Keycloak, stocke les tokens en mémoire JavaScript et rafraîchit automatiquement les tokens expirés. Pour une API REST pure sans interface web, utilisez uniquement keycloak-connect. Pour une application full-stack avec interface utilisateur, combinez les deux.
Comment migrer depuis Passport.js vers Keycloak ?
La migration s’effectue en trois phases sans interruption de service. Phase 1 (semaine 1-2) : déployer Keycloak et importer les utilisateurs existants via l’API REST d’import en masse (POST /admin/realms/{realm}/users) ou via un provider LDAP si les utilisateurs sont dans un annuaire Active Directory. Phase 2 (semaine 3-4) : ajouter keycloak-connect en parallèle de Passport.js avec un flag d’environnement pour router une portion du trafic vers Keycloak (canary release à 10%, puis 50%, puis 100%). Phase 3 (semaine 5-6) : supprimer Passport.js une fois que 100% du trafic est validé sur Keycloak pendant 48 heures sans incident.
Peut-on utiliser Keycloak avec Next.js App Router ?
Oui. Pour Next.js 14+ avec App Router, utilisez @auth/nextjs (anciennement NextAuth.js v5) avec le provider Keycloak OIDC. La configuration requiert l’URL de l’issuer : issuer: "http://localhost:8080/realms/mon-app". @auth/nextjs gère automatiquement PKCE, le stockage sécurisé des tokens via les Server Components (jamais côté client) et le rafraîchissement des tokens. Pour les API Routes Next.js, les tokens sont accessibles côté serveur via auth() sans exposition au navigateur.
Keycloak supporte-t-il les architectures microservices ?
Keycloak est conçu pour les architectures microservices. Chaque service Node.js est un client Keycloak distinct avec ses propres rôles, politiques et scopes. Le token JWT propagé entre services via le header Authorization contient les claims nécessaires pour les décisions d’autorisation locales sans appel réseau supplémentaire (validation locale de signature). Pour les communications service-à-service sans intervention utilisateur, utilisez le Client Credentials Grant : chaque service s’authentifie avec son propre client_id et client_secret, 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écurisée au secret client pour les architectures zero-trust.
Comment détecter les tentatives d’attaque sur Keycloak ?
Keycloak 26.6.0 expose nativement les métriques Prometheus sur le port 9000. La métrique keycloak_failed_login_attempts_total signale les attaques bruteforce : configurez une alerte à partir de 10 échecs par minute sur une même adresse IP. La protection bruteforce intégrée (activée dans ce tutoriel avec failureFactor: 5) verrouille les comptes après 5 tentatives. Intégrez les logs JSON Keycloak (activés avec KC_LOG_FORMAT=json) dans votre SIEM (Elastic, Graylog, Wazuh) pour corréler les événements d’authentification échouée avec d’autres indicateurs de compromission (IP abuse, horaires inhabituels, géolocalisation).
Quelle est la durée de vie recommandée pour les tokens en production ?
Les recommandations de l’ANSSI et du SANS Institute convergent sur les valeurs suivantes pour les API exposées sur internet : access token à 300 secondes (5 minutes), refresh token à 1 800 secondes (30 minutes d’inactivité), session SSO maximale à 8 heures (journée de travail). Pour les applications manipulant des données très sensibles (données de santé, données bancaires), réduisez l’access token à 60 secondes et le refresh token à 900 secondes. La rotation des refresh tokens (refreshTokenMaxReuse: 0) doit être activée dans tous les cas, car elle est le seul mécanisme de détection de rejeu disponible sans introspection.
keycloak-connect est-il maintenu activement en 2026 ?
Oui. keycloak-connect est maintenu par l’équipe officielle Red Hat/Keycloak. La version 26.1.1, publiée en parallèle du serveur Keycloak 26.x, est la dernière version stable au moment de la rédaction de ce tutoriel (juin 2026). Avec 523 125 téléchargements mensuels et une intégration dans le cycle de release officiel Keycloak, le package est activement maintenu. Pour les nouvelles fonctionnalités Keycloak 26.6 (JWT Authorization Grant, DPoP amélioré), vérifiez la roadmap du package sur GitHub pour l’inclusion dans une version keycloak-connect future. En attendant, ces fonctionnalités sont accessibles directement via l’API REST Keycloak sans passer par le middleware.




