Construire une authentification OAuth2 robuste avec Passport

Une authentification par jetons rafraîchissants permet à une application web de maintenir une session sécurisée sans conserver un access token longue durée dans le navigateur. Le principe consiste à utiliser un jeton d’accès court pour appeler les API et un refresh token, plus durable, pour obtenir automatiquement un nouveau jeton lorsque le premier expire.

Dans un environnement Node.js, Passport facilite l’intégration de plusieurs stratégies d’identité : connexion locale, OAuth2, OpenID Connect ou validation de JWT. Il ne gère cependant pas à lui seul toute la logique des refresh tokens. Le serveur doit définir leur stockage, leur rotation, leur révocation et les protections contre le rejeu.

Cette architecture convient aux applications React, aux API Express, aux clients mobiles et aux architectures composées de plusieurs services. Elle sépare clairement l’authentification initiale, l’autorisation des requêtes et le renouvellement de session.

Le modèle présenté s’appuie sur OAuth2, Passport et des JWT signés. Il met l’accent sur le flux Authorization Code avec PKCE, recommandé pour les applications publiques, ainsi que sur les bonnes pratiques de sécurité côté serveur et navigateur.

Élément Durée typique Utilisation Stockage conseillé
Access token 5 à 15 minutes Appels vers les API Mémoire côté client
Refresh token Quelques jours à quelques semaines Obtenir un nouvel access token Cookie HttpOnly sécurisé
Code d’autorisation Quelques minutes Échanger une connexion OAuth2 Mémoire serveur ou URL temporaire
Identifiant de session Variable Retrouver une session révoquée Base de données ou Redis

Comprendre Le Flux OAuth2 Et Les Jetons

Dans le flux Authorization Code, l’utilisateur est redirigé vers le fournisseur d’identité. Après son consentement, l’application reçoit un code temporaire. Le backend échange ce code contre un access token et un refresh token, sans exposer directement les secrets OAuth2 au navigateur.

Avec Passport, une stratégie OAuth2 initialise cette redirection et traite la réponse du fournisseur. Selon le fournisseur utilisé, il peut s’agir de Google, GitHub, Microsoft ou d’un serveur OpenID Connect interne. Le callback doit vérifier l’état de la requête, récupérer le profil et associer l’identité externe à un compte local.

L’access token doit rester très court. S’il est intercepté, sa fenêtre d’utilisation reste limitée. Le refresh token, lui, donne davantage de pouvoir : il ne doit donc pas être envoyé dans les en-têtes de chaque requête API ni exposé au code JavaScript exécuté dans la page.

Un serveur OAuth2 complet peut émettre lui-même ces deux jetons. Dans une application qui délègue la connexion à un fournisseur, il est souvent préférable de conserver une session locale et de générer ses propres jetons internes après validation du profil externe.

Configurer Passport Dans Une API Node.js

L’installation de base comprend Express, Passport, une stratégie OAuth2 ou OpenID Connect, une bibliothèque JWT et un accès à une base de données. La stratégie Passport doit être configurée avec un identifiant client, un secret conservé dans les variables d’environnement et une URL de callback explicitement autorisée.

passport.use("oauth2", new OAuth2Strategy({
  authorizationURL: process.env.AUTHORIZATION_URL,
  tokenURL: process.env.TOKEN_URL,
  clientID: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  callbackURL: process.env.CALLBACK_URL
}, async (accessToken, refreshToken, profile, done) => {
  const user = await users.findOrCreateFromProvider(profile);
  done(null, user);
}));

La route de connexion lance la stratégie avec passport.authenticate("oauth2"). La route de callback récupère l’utilisateur validé, crée une session applicative et pose le refresh token dans un cookie. La réponse peut ensuite rediriger vers le frontend sans inclure de jeton sensible dans l’URL.

Pour une API protégée, passport-jwt vérifie la signature, l’émetteur, l’audience et l’expiration de l’access token. La stratégie ne doit pas seulement décoder le JWT : elle doit aussi rejeter les algorithmes inattendus et imposer une clé de signature connue.

passport.use(new JwtStrategy({
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.ACCESS_TOKEN_SECRET,
  issuer: "mon-api",
  audience: "mon-client"
}, async (payload, done) => {
  const user = await users.findById(payload.sub);
  done(null, user || false);
}));

Implémenter Le Renouvellement Et La Rotation

L’endpoint POST /auth/refresh lit le refresh token depuis un cookie HttpOnly. Le serveur vérifie son empreinte en base, son expiration, son identifiant unique et son association avec une session. Il émet ensuite un nouvel access token et, idéalement, un nouveau refresh token.

La rotation est essentielle. À chaque renouvellement, l’ancien jeton est invalidé et remplacé. Si un ancien refresh token est réutilisé, le serveur peut considérer l’événement comme un signe de vol et révoquer toute la famille de jetons liée à la session.

Il est préférable de stocker uniquement un hash du refresh token, comme pour un mot de passe. La base peut conserver son identifiant, sa date d’expiration, son empreinte, le navigateur concerné et une référence vers la famille de rotation. Une table dédiée facilite la révocation par appareil.

Le cookie doit utiliser HttpOnly, Secure en production et une politique SameSite adaptée au déploiement. Lorsque le frontend et l’API sont sur des domaines différents, la protection CSRF doit être ajoutée avec un token dédié ou une vérification stricte de l’origine.

Un mécanisme de verrouillage côté client évite que plusieurs requêtes expirées déclenchent simultanément plusieurs renouvellements. Le frontend met temporairement les appels en attente, exécute une seule requête de refresh, puis rejoue les appels après réception du nouveau jeton. Cette logique complète une stratégie de cache côté client sans confondre cache de données et session d’identité.

Protéger Le Navigateur Et Les Applications Frontend

Le choix du stockage dépend du niveau de menace. Placer un refresh token dans localStorage le rend accessible à un script injecté lors d’une attaque XSS. Le cookie HttpOnly réduit ce risque, même si une faille XSS peut encore déclencher des actions au nom de l’utilisateur.

L’access token peut rester en mémoire dans une application React. Au rechargement de la page, le client appelle l’endpoint de refresh pour reconstituer une session. Cette approche limite la persistance du jeton et évite de l’exposer dans les outils de stockage du navigateur.

Les applications construites avec différents frameworks doivent conserver le même contrat d’authentification. Une comparaison des architectures frontend, comme cette comparaison Next.js Astro, aide à anticiper les différences entre rendu serveur, génération statique et appels effectués dans le navigateur.

Le serveur doit également appliquer une limitation de débit sur la connexion, le callback OAuth2 et le renouvellement. Les erreurs ne doivent pas révéler si un compte existe. Les journaux peuvent enregistrer l’identifiant de session et l’adresse IP partiellement masquée, sans écrire les tokens ni les données personnelles inutiles.

Tester, Révoquer Et Exploiter Le Système

Les tests doivent couvrir le parcours nominal et les scénarios de compromission. Il faut vérifier l’expiration de l’access token, la rotation du refresh token, le rejeu d’un ancien jeton, la déconnexion d’un seul appareil et la révocation globale d’un compte.

Une stratégie d’observabilité complète suit les taux d’échec du callback, les renouvellements refusés, les anomalies de géolocalisation et les rafales de requêtes. Dans une architecture distribuée, Redis peut servir de stockage rapide pour les sessions révoquées, tandis que la base principale conserve l’historique durable.

Les développeurs peuvent centraliser les composants réutilisables dans une zone d’intégration afin de documenter les stratégies Passport, les middlewares d’autorisation et les exemples de tests. Cette organisation réduit les différences entre les services et accélère les revues de sécurité.

Recommandations Pour Une Mise En Production

Cette conception fournit une base solide pour une authentification OAuth2 moderne avec Passport. En séparant clairement les responsabilités, en limitant la durée des jetons et en surveillant leur cycle de vie, l’application gagne en sécurité sans sacrifier l’expérience utilisateur. Implementée progressivement derrière des tests automatisés et des métriques, elle peut ensuite être étendue à plusieurs fournisseurs d’identité, à la double authentification et à une architecture de microservices.