Authentification et sécurité web - 04 - Refresh tokens : garder l'utilisateur connecte

Les refresh tokens pour renouveler les access tokens sans redemander le mot de passe. Rotation, stockage et sécurité.

04 - Refresh tokens : garder l'utilisateur connecte

Ce que tu vas apprendre

  • Pourquoi les access tokens expirent vite et comment les renouveler
  • Le mecanisme de refresh token rotation
  • Ou stocker les refresh tokens et quoi faire en cas de vol

Prerequisites

Avoir lu les articles sur les sessions, les JWT et OAuth2.


Le problème

Ton access token expire dans une heure. L'utilisateur est en train de remplir un formulaire depuis 45 minutes. Il clique sur "Envoyer" et... 401 Unauthorized. Il doit se reconnecter, son formulaire est perdu, il est furieux.

Première reaction : "je vais mettre une expiration de 30 jours sur l'access token". Mauvaise idee. Plus un token vit longtemps, plus la fenêtre d'exploitation est grande en cas de vol. Un access token vole avec 30 jours de validite, c'est 30 jours d'acces libre a ton API.

La solution : les refresh tokens.

Le mecanisme

L'idee est simple. A la connexion, le serveur emet deux tokens :

  • Un access token avec une duree de vie courte (15 minutes a 1 heure)
  • Un refresh token avec une duree de vie longue (7 a 30 jours)

Quand l'access token expire, le client envoie le refresh token au serveur. Le serveur vérifié le refresh token, en généré un nouveau (avec un nouvel access token), et invalide l'ancien.

typescript// POST /api/auth/refresh
app.post("/api/auth/refresh", async ({ body, cookie }) => {
  const refreshToken = cookie.refresh_token?.value;
  if (!refreshToken) {
    return new Response("Unauthorized", { status: 401 });
  }

  // Verifier que le refresh token existe en base
  const stored = await db.query(
    "SELECT user_id, expires_at, used FROM refresh_tokens WHERE token = $1",
    [refreshToken]
  );

  if (!stored || stored.used || new Date() > stored.expires_at) {
    // Token inexistant, deja utilise, ou expire
    return new Response("Unauthorized", { status: 401 });
  }

  // Marquer l'ancien comme utilise (rotation)
  await db.query("UPDATE refresh_tokens SET used = true WHERE token = $1", [refreshToken]);

  // Generer une nouvelle paire
  const newAccessToken = sign({ userId: stored.user_id }, SECRET, { expiresIn: "1h" });
  const newRefreshToken = crypto.randomUUID();

  await db.query(
    "INSERT INTO refresh_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)",
    [newRefreshToken, stored.user_id, new Date(Date.now() + 30 * 24 * 60 * 3600 * 1000)]
  );

  return new Response(JSON.stringify({ accessToken: newAccessToken }), {
    headers: {
      "Set-Cookie": `refresh_token=${newRefreshToken}; Path=/api/auth; HttpOnly; SameSite=Strict; Max-Age=${30 * 24 * 3600}`,
    },
  });
});

Refresh token rotation

Le détail qui change tout : chaque refresh token est a usage unique. Quand tu l'utilises pour obtenir un nouveau access token, l'ancien refresh token est invalide.

Pourquoi ? Imagine qu'un attaquant vole ton refresh token. Il l'utilise pour obtenir un access token. Toi aussi, tu l'utilises (parce que ton access token a expire). Le serveur voit que le meme refresh token est utilise deux fois. C'est anormal. Il invalide tous les tokens de cet utilisateur et force une reconnexion.

Sans rotation, un refresh token vole donne un acces silencieux pendant toute sa duree de vie. Avec rotation, le vol est détecté a la première utilisation concurrente.

La recommandation d'Auth0 sur la rotation est claire : "Refresh token rotation is a technique for getting new access tokens using refresh tokens that goes beyond silent authentication."

Ou stocker les refresh tokens

Sur le web : cookie HttpOnly. Pas dans localStorage, pas dans sessionStorage, pas dans une variable JavaScript. Un cookie avec HttpOnly; SameSite=Strict; Secure; Path=/api/auth est invisible pour le JavaScript de la page et n'est envoye qu'a l'endpoint de refresh.

Sur mobile (iOS/Android) : le secure storage du système. Keychain sur iOS, EncryptedSharedPreferences sur Android. Pas dans un fichier texte, pas dans les préférences normales.

Cote serveur : en base de donnees. Contrairement aux access tokens (qui sont des JWT verifiables sans base), les refresh tokens doivent etre stockes cote serveur pour pouvoir les revoquer. C'est le coût a payer pour la sécurité.

Quand tu n'as pas besoin de refresh tokens

Si tu utilises des sessions avec cookies (comme sur paltemps.fr), tu n'as pas besoin de refresh tokens. La session a une duree de vie, et le serveur peut la prolonger a chaque requête (sliding window). Le cookie est renvoye automatiquement par le navigateur. Pas de logique cote client.

Les refresh tokens existent pour compenser une limitation des JWT : l'impossibilite de les revoquer. Si tu n'utilises pas de JWT, tu n'as pas ce problème.

Mon conseil : ne rajoute pas de la complexité pour le plaisir. Les refresh tokens sont nécessaires pour les apps mobiles et les architectures JWT. Pour un site web classique, les sessions font le travail plus simplement.


Navigation : Precedent : 03 - OAuth2 | Suivant : 05 - Hashing de mots de passe


Sources

Retrouve d'autres articles techniques sur paltemps.fr.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.