Un identifiant volé reste la porte d’entrée numéro un des attaquants. En 2026, déléguer l’authentification à un fournisseur d’identité via OAuth2 et OpenID Connect n’est plus une option avancée, c’est la base d’une application web sérieuse. Ce tutoriel vous guide pas à pas pour construire, en Node.js, un flux d’authentification complet, conforme à OAuth 2.1, avec PKCE, vérification d’ID token, sessions chiffrées et rotation des refresh tokens.
Vous repartirez avec un projet fonctionnel : un serveur Express qui redirige l’utilisateur vers un fournisseur OIDC, échange le code d’autorisation contre des jetons, sécurise la session par cookie et détecte le vol de refresh token. Comptez environ 45 minutes pour suivre les 12 étapes. Le code utilise openid-client 6.8.4, la bibliothèque de référence maintenue par Filip Skokan, et jose 6.2.3 pour la cryptographie des jetons.
OAuth2 et OpenID Connect : comprendre la différence avant de coder
La confusion entre OAuth2 et OpenID Connect provoque la majorité des failles d’authentification. Les deux protocoles cohabitent dans le même flux, mais ils répondent à deux questions distinctes. OAuth2 (défini par la RFC 6749) est un cadre d’autorisation. Il répond à : « cette application a-t-elle le droit d’accéder à cette ressource ? ». Son livrable est l’access token, un jeton que le client présente à une API pour obtenir des données.
OpenID Connect (OIDC) est une couche d’identité posée sur OAuth2. Il répond à : « qui est cet utilisateur ? ». Son livrable est l’ID token, un JWT signé qui prouve l’identité du visiteur et contient des « claims » (sujet, email, nom, date d’émission). Utiliser un access token pour authentifier un utilisateur est une erreur classique : l’access token est destiné à une API, pas à votre application. Il n’est pas conçu pour être validé côté client, et plusieurs failles documentées découlent de cette confusion.
Concrètement, dans le flux que nous construisons, le fournisseur d’identité (Auth0, Keycloak, Google, Microsoft Entra ID) nous renvoie trois jetons : un ID token pour savoir qui se connecte, un access token pour appeler des API, et un refresh token pour renouveler la session sans redemander le mot de passe. Notre application valide l’ID token, crée une session locale, et conserve le refresh token de façon sécurisée. Si vous avez déjà suivi notre tutoriel sur l’authentification JWT en Node.js, vous reconnaîtrez la structure des jetons : OIDC standardise simplement leur émission par un tiers de confiance.
Pourquoi OAuth 2.1 redéfinit les bonnes pratiques en 2026
OAuth 2.1 ne réinvente rien. Il consolide quinze ans de correctifs de sécurité épars en une spécification unique, et il rend obligatoires des mesures qui étaient jusqu’ici recommandées. Trois changements structurent toute implémentation moderne et conditionnent le code de ce tutoriel.
Premièrement, PKCE (Proof Key for Code Exchange, RFC 7636) devient obligatoire pour le flux Authorization Code, pour tous les clients, y compris les clients confidentiels côté serveur. PKCE empêche l’interception du code d’autorisation : même si un attaquant capture le code, il ne peut pas l’échanger sans le « code verifier » secret généré par le client légitime.
Deuxièmement, le grant implicite (qui renvoyait l’access token directement dans l’URL) est supprimé, tout comme le grant « Resource Owner Password Credentials ». Ces deux flux exposaient les jetons dans l’historique du navigateur ou demandaient à l’application de manipuler le mot de passe de l’utilisateur. Désormais, seul le flux Authorization Code avec PKCE est recommandé pour les applications web.
Troisièmement, la rotation des refresh tokens devient la norme pour les clients publics. La RFC 9700 (Best Current Practice for OAuth 2.0 Security, publiée par l’IETF) formalise la détection de réutilisation : si un ancien refresh token réapparaît après rotation, le serveur révoque toute la « famille » de jetons. C’est exactement ce que nous implémenterons aux étapes 9 et 10. Le contexte européen renforce cette exigence : la directive NIS2, désormais en vigueur, étend les obligations de sécurité à entre 15 000 et 18 000 organisations en France selon la transposition nationale, et l’authentification résistante au hameçonnage figure au cœur des recommandations de l’ANSSI et de l’ENISA.
Prérequis et versions logicielles
Avant de commencer, vérifiez que votre environnement correspond aux versions ci-dessous. La bibliothèque openid-client a connu une réécriture majeure en version 6 : elle est désormais « ESM-first » et s’appuie sur les standards web (Fetch, WebCrypto). Le code de ce tutoriel ne fonctionne pas avec les anciennes versions 5.x dont l’API diffère totalement.
| Composant | Version utilisée | Rôle dans le projet |
|---|---|---|
| Node.js | 24.x LTS (Krypton) | Runtime, support jusqu’en avril 2028 |
| Express | 5.2.1 | Serveur HTTP et routage |
| openid-client | 6.8.4 | Découverte OIDC, PKCE, échange de jetons |
| jose | 6.2.3 | Vérification de signature JWT/ID token |
| cookie-session | 2.1.1 | Session chiffrée côté cookie |
| dotenv | latest | Chargement des variables d’environnement |
Côté connaissances, vous devez être à l’aise avec JavaScript asynchrone (async/await), les modules ES (import/export) et les bases de HTTP. Il vous faut aussi un compte chez un fournisseur OIDC. Ce tutoriel utilise une configuration générique compatible avec Auth0, Keycloak, Okta ou Microsoft Entra ID. Enfin, exécutez tout en HTTPS dès que possible : sans TLS, les cookies de session et les jetons circulent en clair. Notre guide pour obtenir un certificat SSL gratuit avec Certbot couvre cette mise en place, et notre explication sur HTTPS et TLS détaille pourquoi ce socle est non négociable.
Étape 1 : initialiser le projet Node.js
Créez un répertoire et initialisez le projet. Le point essentiel : déclarez "type": "module" dans le package.json, car openid-client 6 est distribué exclusivement en modules ES. Sans cette ligne, les import échoueront avec une erreur ERR_REQUIRE_ESM.
mkdir oauth2-oidc-nodejs && cd oauth2-oidc-nodejs
npm init -y
npm install [email protected] [email protected] [email protected] [email protected] dotenv
# Activer les modules ES
npm pkg set type="module"
npm pkg set engines.node=">=24.0.0"
Créez ensuite un fichier .env à la racine. Ne le versionnez jamais : ajoutez-le immédiatement à votre .gitignore. Les valeurs proviennent de la console de votre fournisseur d’identité, dans la section « Applications » où vous enregistrez un client web.
# .env
OIDC_ISSUER=https://votre-tenant.eu.auth0.com
OIDC_CLIENT_ID=AbCdEf123456
OIDC_CLIENT_SECRET=votre_secret_client
OIDC_REDIRECT_URI=https://localhost:3000/callback
SESSION_KEY=une_cle_aleatoire_de_32_octets_minimum
PORT=3000
Générez la SESSION_KEY avec une vraie source d’entropie, jamais une chaîne devinée. La commande node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" produit une clé de 32 octets adaptée au chiffrement des cookies de session.
Étape 2 : enregistrer l’application chez le fournisseur d’identité
Dans la console de votre fournisseur OIDC, créez une application de type « Regular Web Application » (client confidentiel). Trois réglages sont critiques et causent la plupart des erreurs de débutant.
- Allowed Callback URLs : renseignez exactement
https://localhost:3000/callback. La moindre différence (slash final, http au lieu de https, port absent) déclenche une erreurredirect_uri_mismatch. - Grant Types : activez uniquement « Authorization Code » et « Refresh Token ». Désactivez « Implicit » : il est supprimé par OAuth 2.1.
- Refresh Token Rotation : activez la rotation si votre fournisseur le propose (Auth0, Keycloak). Définissez aussi une durée de vie absolue pour limiter la fenêtre d’exploitation en cas de vol.
Notez l’« Issuer URL ». C’est la racine à partir de laquelle openid-client découvrira automatiquement tous les endpoints (autorisation, jeton, JWKS) via le document /.well-known/openid-configuration. Cette découverte automatique évite de coder en dur des URL qui changent selon le fournisseur.
Étape 3 : configurer openid-client 6 et la découverte OIDC
Créez un fichier oidc.js qui centralise la configuration. La fonction discovery() de la version 6 récupère les métadonnées du fournisseur et construit un objet Configuration réutilisable. C’est le point d’entrée de toute l’API moderne de la bibliothèque.
// oidc.js
import * as client from 'openid-client';
import 'dotenv/config';
let config;
export async function getConfig() {
if (config) return config;
const issuer = new URL(process.env.OIDC_ISSUER);
// Decouverte automatique via /.well-known/openid-configuration
config = await client.discovery(
issuer,
process.env.OIDC_CLIENT_ID,
process.env.OIDC_CLIENT_SECRET
);
return config;
}
Le mécanisme de découverte télécharge aussi l’URL du JWKS (JSON Web Key Set), l’ensemble des clés publiques qui serviront à vérifier la signature de l’ID token à l’étape 7. La bibliothèque met ces clés en cache et gère leur rotation automatiquement. Vous n’avez donc jamais à manipuler de certificat manuellement, contrairement à une vérification de signature numérique que l’on coderait de zéro.
Étape 4 : générer le code verifier et le code challenge (PKCE)
PKCE repose sur un secret éphémère. Le client génère un code verifier (chaîne aléatoire), en dérive un code challenge par hachage SHA-256, envoie le challenge au fournisseur lors de la redirection, puis prouve son identité en révélant le verifier au moment de l’échange du code. Un attaquant qui intercepte le code d’autorisation ne possède pas le verifier et ne peut donc rien en faire.
Ajoutez aussi un paramètre state (anti-CSRF) et un nonce (anti-rejeu de l’ID token). La bibliothèque fournit des générateurs sûrs pour chacun. Ces valeurs doivent être stockées dans la session le temps de l’aller-retour.
// auth-helpers.js
import * as client from 'openid-client';
export function createPkcePair() {
const code_verifier = client.randomPKCECodeVerifier();
return code_verifier;
}
export async function buildAuthParams(config, code_verifier) {
const code_challenge =
await client.calculatePKCECodeChallenge(code_verifier);
return {
redirect_uri: process.env.OIDC_REDIRECT_URI,
scope: 'openid profile email offline_access',
code_challenge,
code_challenge_method: 'S256',
state: client.randomState(),
nonce: client.randomNonce(),
};
}
Le scope offline_access demande explicitement un refresh token : sans lui, le fournisseur n’en émet pas, et la rotation des étapes 9 et 10 n’aurait aucun objet. Le code_challenge_method doit valoir S256 (SHA-256) et jamais plain, qui n’apporte aucune protection réelle.
Étape 5 : construire la route de connexion (/login)
Assemblons le serveur Express. La route /login génère le verifier PKCE, le stocke en session avec le state et le nonce, puis redirige l’utilisateur vers l’endpoint d’autorisation du fournisseur. C’est le départ du flux Authorization Code.
// server.js
import express from 'express';
import cookieSession from 'cookie-session';
import * as client from 'openid-client';
import { getConfig } from './oidc.js';
import { createPkcePair, buildAuthParams } from './auth-helpers.js';
import 'dotenv/config';
const app = express();
app.use(cookieSession({
name: 'sid',
keys: [process.env.SESSION_KEY],
maxAge: 24 * 60 * 60 * 1000, // 24 h
httpOnly: true,
secure: true,
sameSite: 'lax',
}));
app.get('/login', async (req, res) => {
const config = await getConfig();
const code_verifier = createPkcePair();
const params = await buildAuthParams(config, code_verifier);
// Stockage temporaire pour valider le retour
req.session.code_verifier = code_verifier;
req.session.state = params.state;
req.session.nonce = params.nonce;
const authUrl = client.buildAuthorizationUrl(config, params);
res.redirect(authUrl.href);
});
Notez les options du cookie de session : httpOnly bloque l’accès JavaScript (protection contre le vol par XSS), secure impose HTTPS, et sameSite: 'lax' limite l’envoi du cookie sur des requêtes cross-site, ce qui contre une grande partie des attaques CSRF. Ces trois réglages sont le minimum pour un cookie de session en 2026.
Étape 6 : gérer le callback et échanger le code
Après authentification, le fournisseur redirige l’utilisateur vers /callback avec le code d’autorisation et le state. La fonction authorizationCodeGrant() vérifie le state, échange le code contre les jetons en présentant le code verifier, et valide automatiquement la signature et le nonce de l’ID token. C’est l’étape la plus dense du flux.
app.get('/callback', async (req, res) => {
const config = await getConfig();
const currentUrl = new URL(
req.originalUrl, `https://localhost:${process.env.PORT}`
);
try {
const tokens = await client.authorizationCodeGrant(
config,
currentUrl,
{
pkceCodeVerifier: req.session.code_verifier,
expectedState: req.session.state,
expectedNonce: req.session.nonce,
}
);
const claims = tokens.claims();
// Nettoyage des valeurs temporaires
delete req.session.code_verifier;
delete req.session.state;
delete req.session.nonce;
// Creation de la session applicative
req.session.user = {
sub: claims.sub,
email: claims.email,
name: claims.name,
};
req.session.refresh_token = tokens.refresh_token;
req.session.access_token = tokens.access_token;
req.session.token_family = client.randomState();
res.redirect('/profile');
} catch (err) {
console.error('Echec du callback OIDC :', err.message);
res.status(401).send('Authentification echouee');
}
});
La méthode tokens.claims() renvoie le contenu déjà validé de l’ID token : inutile de revérifier la signature manuellement ici, la bibliothèque l’a fait. Le champ token_family que nous ajoutons servira à la détection de réutilisation : tous les refresh tokens issus d’une même connexion partagent cet identifiant de famille.
Étape 7 : vérifier un ID token manuellement avec jose
Dans la plupart des cas, openid-client valide l’ID token pour vous. Mais vous aurez besoin d’une vérification manuelle dès que vous transmettez un jeton à un autre service (microservices) ou que vous recevez un access token au format JWT côté API. La bibliothèque jose est l’outil de référence pour cela. Elle récupère les clés publiques du fournisseur via son JWKS et vérifie la signature, l’émetteur, l’audience et l’expiration.
// verify-token.js
import * as jose from 'jose';
const JWKS = jose.createRemoteJWKSet(
new URL(`${process.env.OIDC_ISSUER}/.well-known/jwks.json`)
);
export async function verifyIdToken(idToken) {
const { payload } = await jose.jwtVerify(idToken, JWKS, {
issuer: process.env.OIDC_ISSUER,
audience: process.env.OIDC_CLIENT_ID,
maxTokenAge: '1h',
});
return payload;
}
createRemoteJWKSet met les clés en cache et gère leur rotation : si le fournisseur change de clé de signature, jose récupère automatiquement la nouvelle. Ne désactivez jamais la vérification de l’audience ni de l’émetteur. Un jeton valide émis pour une autre application ne doit jamais être accepté par la vôtre, faute de quoi vous ouvrez une faille de « token substitution ».
Étape 8 : sécuriser la session par cookie chiffré
Le choix du stockage de session est une décision de sécurité. Deux approches dominent. La première, le cookie de session signé et chiffré (notre cookie-session), stocke les données chez le client : simple, sans état serveur, mais limité en taille et difficile à révoquer immédiatement. La seconde, la session côté serveur (Redis, base de données), permet une révocation instantanée mais demande une infrastructure.
| Réglage cookie | Valeur recommandée | Menace contrée |
|---|---|---|
| httpOnly | true | Vol de cookie via XSS |
| secure | true | Interception sur HTTP en clair |
| sameSite | lax (ou strict) | CSRF cross-site |
| maxAge | court (1 à 24 h) | Fenêtre de session volée |
| domain/path | le plus restrictif | Fuite vers sous-domaines |
Pour une application sensible (banque, santé, secteur soumis à NIS2), privilégiez la session serveur avec Redis : elle seule permet de révoquer une session en cas de compromission détectée. Pour ce tutoriel, le cookie chiffré suffit à illustrer le flux. Quelle que soit l’option, ne stockez jamais le refresh token dans un cookie accessible au JavaScript, ni dans le localStorage : ce sont les cibles favorites des attaques XSS. La gestion des secrets reste un sujet à part entière, complémentaire de la sécurité des mots de passe côté utilisateur.
Étape 9 : implémenter la rotation des refresh tokens
Voici le cœur de la sécurité moderne. Quand l’access token expire (typiquement après 15 minutes à 1 heure), le client utilise le refresh token pour en obtenir un nouveau sans redemander le mot de passe. Avec la rotation, le fournisseur renvoie aussi un nouveau refresh token et invalide l’ancien. Chaque renouvellement remplace donc le jeton précédent.
// refresh.js
import * as client from 'openid-client';
import { getConfig } from './oidc.js';
export async function refreshSession(req) {
const config = await getConfig();
const oldRefreshToken = req.session.refresh_token;
if (!oldRefreshToken) {
throw new Error('Aucun refresh token en session');
}
const tokens = await client.refreshTokenGrant(
config,
oldRefreshToken
);
// Rotation : on remplace l'ancien jeton par le nouveau
req.session.access_token = tokens.access_token;
if (tokens.refresh_token) {
req.session.refresh_token = tokens.refresh_token;
}
return tokens;
}
Le point clé : après l’appel, l’ancien refresh token ne doit plus jamais servir. Si votre fournisseur a la rotation activée (configurée à l’étape 2), il invalide automatiquement l’ancien jeton côté serveur. Votre code doit refléter cette logique en remplaçant immédiatement la valeur en session. Un refresh token qui vit indéfiniment équivaut à un mot de passe permanent volable : c’est précisément ce que la rotation élimine.
Étape 10 : détecter la réutilisation de token (reuse detection)
La rotation seule ne suffit pas. Imaginez qu’un attaquant vole un refresh token et l’utilise avant la victime. Le serveur émet un nouveau jeton à l’attaquant et invalide l’ancien. Quand la victime légitime tente ensuite d’utiliser son jeton (l’ancien, désormais invalide), le serveur détecte qu’un jeton déjà tourné réapparaît. C’est le signal d’un vol. La réponse correcte : révoquer toute la famille de jetons, forçant attaquant et victime à se reconnecter.
Côté application, vous tenez un registre des familles de jetons (en base ou en Redis). Voici une implémentation minimale avec une Map en mémoire, à remplacer par un stockage persistant en production.
// reuse-detection.js
const tokenFamilies = new Map(); // famille -> { active: Set, revoked: bool }
export function registerToken(family, jti) {
if (!tokenFamilies.has(family)) {
tokenFamilies.set(family, { active: new Set(), revoked: false });
}
tokenFamilies.get(family).active.add(jti);
}
export function rotateToken(family, oldJti, newJti) {
const f = tokenFamilies.get(family);
if (!f || f.revoked) return false;
// Reutilisation d'un jeton deja retire = vol probable
if (!f.active.has(oldJti)) {
f.revoked = true; // on revoque toute la famille
f.active.clear();
console.warn(`ALERTE : reutilisation detectee, famille ${family} revoquee`);
return false;
}
f.active.delete(oldJti);
f.active.add(newJti);
return true;
}
En pratique, beaucoup de fournisseurs (Auth0, Keycloak, Okta) gèrent eux-mêmes la détection de réutilisation quand vous activez la rotation. Implémenter cette logique côté application reste utile pour les architectures où vous émettez vos propres refresh tokens, ou pour journaliser les tentatives de vol à des fins de surveillance, une exigence courante des audits de conformité européens.
Étape 11 : protéger les routes avec un middleware
Un middleware Express vérifie qu’une session valide existe avant d’autoriser l’accès aux routes protégées. S’il manque un utilisateur en session, on redirige vers /login. Si l’access token a expiré, on tente un renouvellement silencieux via la rotation de l’étape 9.
import { refreshSession } from './refresh.js';
async function requireAuth(req, res, next) {
if (!req.session.user) {
return res.redirect('/login');
}
try {
// Renouvellement silencieux si l'access token est expire
if (isAccessTokenExpired(req.session.access_token)) {
await refreshSession(req);
}
next();
} catch (err) {
req.session = null; // on detruit la session corrompue
res.redirect('/login');
}
}
app.get('/profile', requireAuth, (req, res) => {
res.json({
message: 'Zone protegee',
user: req.session.user,
});
});
Ce schéma centralise le contrôle d’accès : aucune route métier ne manipule les jetons directement. Pour un contrôle plus fin (rôles, permissions), inspectez les claims de l’ID token ou un access token de type JWT. La défense en profondeur reste la règle : ce middleware complète, sans remplacer, des protections comme le filtrage d’IP ou un outil tel que Fail2ban pour limiter les tentatives.
Étape 12 : déconnexion et révocation des jetons
Une déconnexion correcte fait deux choses : elle détruit la session locale, et elle révoque les jetons auprès du fournisseur via le « RP-Initiated Logout » d’OIDC. Détruire seulement le cookie laisse le refresh token valide côté fournisseur, ce qui est insuffisant.
app.get('/logout', async (req, res) => {
const config = await getConfig();
const refreshToken = req.session.refresh_token;
// Revocation cote fournisseur si supporte
try {
if (refreshToken) {
await client.tokenRevocation(config, refreshToken);
}
} catch (err) {
console.error('Revocation impossible :', err.message);
}
// Destruction de la session locale
req.session = null;
// Deconnexion globale OIDC
const endSessionUrl = client.buildEndSessionUrl(config, {
post_logout_redirect_uri: 'https://localhost:3000/',
});
res.redirect(endSessionUrl.href);
});
app.listen(process.env.PORT, () => {
console.log(`Serveur demarre sur https://localhost:${process.env.PORT}`);
});
Le projet est maintenant complet : connexion, callback sécurisé, sessions, rotation, détection de réutilisation, routes protégées et déconnexion globale. Les douze étapes forment un flux d’authentification conforme à OAuth 2.1.
Choisir son fournisseur d’identité OIDC en Europe
Le code de ce tutoriel reste identique quel que soit le fournisseur, grâce à la découverte automatique de l’étape 3. Mais le choix du fournisseur engage votre conformité, votre budget et votre souveraineté des données, des critères devenus centraux dans le contexte réglementaire européen. Quatre options dominent le marché, chacune avec ses arbitrages.
Keycloak est la solution open source de référence, maintenue par la fondation CNCF. Vous l’auto-hébergez, ce qui garantit que les identités et les jetons ne quittent jamais votre infrastructure : un atout décisif pour les organismes publics français et les entités soumises à NIS2 qui veulent la maîtrise complète de leurs données. En contrepartie, vous assumez l’exploitation, les mises à jour de sécurité et la haute disponibilité. La rotation des refresh tokens et la détection de réutilisation y sont natives et configurables finement.
Auth0 (groupe Okta) offre l’expérience développeur la plus fluide, avec une console claire et une documentation abondante. Son offre gratuite suffit aux petits projets. Le point d’attention en Europe concerne la localisation des données : choisissez explicitement la région européenne lors de la création du tenant pour rester aligné avec le RGPD. Microsoft Entra ID (ex-Azure AD) s’impose naturellement dans les environnements déjà investis dans Microsoft 365, avec une intégration native aux annuaires d’entreprise. ProtonPass, Ory ou Zitadel complètent le paysage pour qui cherche une alternative européenne ou un socle open source moderne.
| Fournisseur | Modèle | Hébergement | Rotation native | Cas d’usage type |
|---|---|---|---|---|
| Keycloak | Open source | Auto-hébergé | Oui | Souveraineté, secteur public |
| Auth0 | SaaS (offre gratuite) | Cloud (région UE possible) | Oui | Startups, prototypage rapide |
| Microsoft Entra ID | SaaS | Cloud Microsoft | Oui | Entreprises Microsoft 365 |
| Okta | SaaS | Cloud | Oui | Grandes entreprises B2B |
| Zitadel | Open source | Auto-hébergé ou cloud UE | Oui | Alternative européenne moderne |
Notre conseil : pour un projet souverain ou soumis à NIS2, partez sur Keycloak auto-hébergé ; pour un produit qui doit aller vite sans équipe dédiée à l’identité, Auth0 ou Zitadel en région européenne offrent le meilleur compromis. Dans tous les cas, vérifiez que le fournisseur supporte PKCE obligatoire, la rotation des refresh tokens et le RP-Initiated Logout, les trois piliers de notre implémentation. Un fournisseur qui ne propose pas la rotation vous oblige à émettre vos propres refresh tokens, un travail bien plus risqué.
Tester le flux complet : exemples de sortie
Lancez le serveur avec node server.js et ouvrez https://localhost:3000/login. Vous êtes redirigé vers le fournisseur, vous vous authentifiez, puis vous revenez sur /profile. Voici la sortie attendue de la route protégée après une connexion réussie.
$ curl -sk https://localhost:3000/profile -b "sid=..."
{
"message": "Zone protegee",
"user": {
"sub": "auth0|6612a8f3c9e1b2",
"email": "[email protected]",
"name": "Marie Dupont"
}
}
Pour vérifier la rotation, déclenchez un renouvellement et observez les journaux. Un refresh réussi affiche le remplacement du jeton ; une tentative de réutilisation déclenche l’alerte de l’étape 10.
$ node server.js
Serveur demarre sur https://localhost:3000
[refresh] access_token renouvele, nouveau refresh_token emis
[refresh] access_token renouvele, nouveau refresh_token emis
ALERTE : reutilisation detectee, famille a1b2c3d4 revoquee
La dernière ligne est le comportement de sécurité recherché : un ancien refresh token réapparaît, le système le traite comme un vol et invalide toute la famille. Dans un test légitime, vous ne devez jamais voir cette alerte.
6 pièges courants à éviter
Ces erreurs reviennent dans presque toutes les premières implémentations. Les connaître à l’avance vous épargne des heures de débogage et, surtout, des failles silencieuses.
- Authentifier l’utilisateur avec l’access token. L’access token est destiné aux API, pas à votre application. Utilisez toujours l’ID token (et ses claims validés) pour identifier qui se connecte.
- Stocker le refresh token côté navigateur. Ni
localStorage, ni cookie accessible au JavaScript. Le refresh token reste côté serveur ou dans un cookiehttpOnlychiffré. - Oublier la validation du state et du nonce. Sans state, vous êtes vulnérable au CSRF sur le callback. Sans nonce, l’ID token peut être rejoué.
- Utiliser
code_challenge_method=plain. SeulS256apporte une protection réelle. La méthodeplaintransmet le verifier en clair. - Ne pas demander le scope
offline_access. Sans lui, aucun refresh token n’est émis, et votre logique de rotation tourne à vide. - Faire confiance à un ID token sans vérifier l’audience. Un jeton valide émis pour une autre application ne doit jamais être accepté par la vôtre.
Dépannage : 8 erreurs fréquentes et leurs solutions
Voici les messages d’erreur que vous rencontrerez le plus souvent, avec leur cause réelle et la correction à appliquer.
| Erreur | Cause probable | Solution |
|---|---|---|
| redirect_uri_mismatch | URL de callback non identique à celle enregistrée | Vérifier slash final, http/https et port à l’identique |
| ERR_REQUIRE_ESM | type: module absent du package.json | Exécuter npm pkg set type="module" |
| invalid_grant | Code déjà utilisé ou expiré (durée ~30 s) | Ne pas rejouer un callback ; relancer /login |
| PKCE verification failed | code_verifier perdu entre /login et /callback | Vérifier que la session persiste (cookie secure en HTTPS) |
| invalid_client | CLIENT_ID ou CLIENT_SECRET incorrect | Recopier les valeurs depuis la console du fournisseur |
| Pas de refresh_token reçu | Scope offline_access manquant | Ajouter offline_access au scope |
| JWKS could not be fetched | Issuer mal formé ou réseau bloqué | Tester l’URL /.well-known/openid-configuration |
| Signature verification failed | Audience ou issuer non concordant | Aligner audience sur CLIENT_ID et issuer sur OIDC_ISSUER |
Pour les erreurs de session (PKCE verification failed en particulier), le coupable est presque toujours le cookie : en développement local sans HTTPS, un cookie secure: true n’est pas envoyé, donc le code_verifier disparaît entre les deux requêtes. La solution propre est d’activer HTTPS en local, pas de désactiver secure.
Astuces avancées pour la production
Le projet fonctionne, mais une mise en production exige des renforcements supplémentaires. Voici les optimisations qui distinguent un prototype d’une application prête pour un audit NIS2.
- Migrez vers une session serveur (Redis). Elle permet la révocation instantanée, indispensable en cas d’incident. Le cookie ne contient alors qu’un identifiant de session opaque.
- Réduisez la durée de vie des access tokens à 15 minutes. Combinée à la rotation des refresh tokens, cette fenêtre courte limite drastiquement l’impact d’un vol.
- Activez DPoP ou mTLS pour les jetons liés au client. Ces mécanismes lient le jeton à une clé du client, rendant un jeton volé inutilisable ailleurs.
- Journalisez chaque révocation de famille. Une réutilisation détectée est un signal d’incident à remonter à votre SIEM.
- Verrouillez les en-têtes HTTP avec helmet. CSP stricte, HSTS et X-Content-Type-Options réduisent la surface d’attaque XSS qui menacerait vos sessions.
- Limitez le débit sur /login et /callback. Un rate limiting empêche le bourrage de codes et les attaques par force brute sur le flux d’autorisation.
Enfin, gardez vos dépendances à jour. L’incident du ver Shai-Hulud, qui a compromis 18 paquets npm totalisant plus de 2,6 milliards de téléchargements hebdomadaires en septembre 2025 avant qu’une variante n’atteigne environ 700 paquets en novembre, rappelle que la chaîne d’approvisionnement logicielle est une cible. Épinglez vos versions, vérifiez les signatures et auditez régulièrement votre arbre de dépendances.
Questions fréquentes sur OAuth2 et OpenID Connect en Node.js
Faut-il PKCE pour un client confidentiel côté serveur ?
Oui. OAuth 2.1 rend PKCE obligatoire pour tous les clients utilisant le flux Authorization Code, y compris les clients confidentiels avec secret. PKCE protège contre l’interception du code, une menace indépendante du secret client. Il n’y a aucune raison de s’en passer.
Quelle est la différence entre access token et ID token ?
L’access token autorise l’accès à une API : votre application le transmet, sans le lire, à un service tiers. L’ID token prouve l’identité de l’utilisateur à votre application : il est destiné à être lu et validé par vous. Confondre les deux est la faille d’authentification OIDC la plus répandue.
openid-client ou Passport.js : que choisir en 2026 ?
openid-client est certifié par l’OpenID Foundation et implémente fidèlement les spécifications, PKCE et rotation incluses. Passport.js reste populaire mais sa stratégie OIDC ajoute une couche d’abstraction qui masque des détails de sécurité. Pour une implémentation OIDC stricte et maintenue, openid-client 6 est le choix de référence.
Où stocker le refresh token en toute sécurité ?
Côté serveur : en base de données ou Redis chiffré, jamais exposé au navigateur. Si vous devez le conserver côté client, utilisez exclusivement un cookie httpOnly, secure et sameSite. Bannissez le localStorage, accessible au moindre script XSS.
La rotation des refresh tokens suffit-elle contre le vol ?
Non, seule. La rotation doit s’accompagner de la détection de réutilisation : c’est elle qui transforme un ancien jeton réapparu en signal d’alerte et révoque toute la famille. La rotation limite la durée de vie ; la détection neutralise l’attaquant. Les deux sont complémentaires.
Ce flux est-il conforme aux exigences NIS2 ?
Le flux décrit (authentification forte déléguée, PKCE, rotation, journalisation des incidents) couvre les attentes d’authentification résistante au hameçonnage que recommandent l’ANSSI et l’ENISA. La conformité NIS2 globale dépend toutefois de l’ensemble de votre dispositif : gestion des accès, surveillance, réponse à incident et tests indépendants.
Faut-il vérifier l’ID token si openid-client le fait déjà ?
Pas dans le callback : la bibliothèque valide signature, nonce, issuer et audience pour vous. La vérification manuelle avec jose devient nécessaire quand vous propagez un jeton vers un microservice ou recevez un JWT côté API, là où aucune validation préalable n’a eu lieu.
Pourquoi mes cookies disparaissent-ils en développement local ?
Parce que secure: true empêche l’envoi du cookie sur une connexion HTTP non chiffrée. La bonne solution est d’activer HTTPS en local (certificat auto-signé ou mkcert), pas de désactiver l’option secure, qui doit rester active partout.
Ressources et articles liés
- Authentification JWT en Node.js : 12 étapes [2026]
- Signatures numériques : comment le hachage et les clés garantissent l’authenticité
- Certificat SSL gratuit avec Certbot : 11 étapes [2026]
- HTTPS et TLS : comment votre connexion est protégée
- Sécurité des mots de passe : longueur, hachage et gestionnaires
- Fail2ban : protéger SSH en 12 étapes [2026]
- Sécurité en ligne : protéger ses données et ses comptes
Pour approfondir les spécifications, consultez les sources de référence : la spécification OAuth 2.1, la RFC 7636 sur PKCE, la RFC 9700 (bonnes pratiques de sécurité OAuth), le guide OpenID Connect de l’OpenID Foundation et le dépôt officiel de openid-client.




