Aller au contenu

Comment sécuriser les cookies de session pour Strapi et Next.js

Apprenez à configurer HttpOnly, Secure et SameSite, à faire pivoter les refresh tokens et à vous protéger contre les attaques CSRF avec Strapi 5 et Next.js.

Par l’équipe Revisionary · · 4 min de lecture

a white google logo on a green background
Photo de Rubaitul Azad sur Unsplash

Strapi 5 et Next.js peuvent partager un flux d’authentification sécurisé lorsqu’on configure correctement les cookies de session et qu’on impose la rotation des tokens. En définissant les attributs HttpOnly, Secure et le bon SameSite, en stockant les tokens d’accès à courte durée en mémoire, et en utilisant des refresh tokens rotatifs, vous protégez les utilisateurs contre les attaques XSS, CSRF et le détournement de session.

1. Pourquoi les paramètres des cookies importent

  • HttpOnly : empêche le JavaScript d’accéder au cookie, limitant le vol par XSS.
  • Secure : le cookie n’est envoyé qu’en HTTPS, évitant les interceptions en mode man‑in‑the‑middle.
  • SameSite : contrôle le moment où le navigateur inclut le cookie, bloquant la plupart des attaques CSRF.
  • Expiration courte : réduit la fenêtre d’attaque par relecture.

Si l’un de ces indicateurs manque, un attaquant capable d’injecter du script ou de piéger un utilisateur sur un site malveillant pourra récupérer ou exploiter la session.

2. Flux d’authentification recommandé

  1. L’utilisateur se connecte → Strapi valide les identifiants et délivre un JWT d’accès signé (15‑30 minutes) ainsi qu’un refresh token (7 jours).
  2. Le token d’accès est stocké dans une variable mémoire côté client (jamais dans le localStorage ni dans les cookies).
  3. Le refresh token est stocké dans un cookie Secure, HttpOnly, SameSite=Strict.
  4. Lorsque le token d’accès expire, le client envoie une requête vers /auth/refresh avec le cookie de rafraîchissement ; Strapi le valide, fait pivoter le refresh token et renvoie un nouveau token d’accès.
  5. Lors de la déconnexion, le cookie de rafraîchissement est supprimé et le serveur révoque le token.

Ce schéma suit les recommandations OWASP pour l’authentification basée sur des tokens et évite les écueils des cookies à longue durée.

Le plugin d’authentification intégré de Strapi peut être étendu avec un contrôleur personnalisé qui émet le cookie de rafraîchissement. Dans src/api/auth/controllers/custom.js :

js
// src/api/auth/controllers/custom.js
import jwt from 'jsonwebtoken';
import { getService } from '@strapi/utils';

export default {
  async login(ctx) {
    const { identifier, password } = ctx.request.body;
    const user = await getService('user').validateUser(identifier, password);
    if (!user) {
      return ctx.badRequest('Invalid credentials');
    }

    // Token d’accès – courte durée, renvoyé dans le corps de la réponse
    const accessToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' });

    // Refresh token – longue durée, stocké dans un cookie HttpOnly
    const refreshToken = jwt.sign({ id: user.id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' });

    ctx.cookies.set('refreshToken', refreshToken, {
      httpOnly: true,
      secure: true, // nécessite HTTPS
      sameSite: 'strict',
      path: '/',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours en ms
    });

    ctx.send({ accessToken });
  },

  async refresh(ctx) {
    const token = ctx.cookies.get('refreshToken');
    if (!token) return ctx.unauthorized('No refresh token');

    try {
      const payload = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
      // Rotation du token
      const newRefresh = jwt.sign({ id: payload.id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' });
      ctx.cookies.set('refreshToken', newRefresh, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        path: '/',
        maxAge: 7 * 24 * 60 * 60 * 1000,
      });

      const newAccess = jwt.sign({ id: payload.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
      ctx.send({ accessToken: newAccess });
    } catch (err) {
      return ctx.unauthorized('Invalid refresh token');
    }
  },

  async logout(ctx) {
    ctx.cookies.set('refreshToken', null, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 0 });
    ctx.send({ message: 'Logged out' });
  },
};

Points clés

  • httpOnly: true bloque l’accès côté client.
  • secure: true force le HTTPS ; en développement local vous pouvez le conditionner.
  • sameSite: 'strict' empêche l’envoi du cookie lors de requêtes inter‑sites.
  • Le refresh token est rotatif à chaque utilisation, limitant les relectures.

3.1 Enregistrement des routes personnalisées

Ajoutez les routes dans src/api/auth/routes/custom.js :

js
// src/api/auth/routes/custom.js
module.exports = {
  routes: [
    { method: 'POST', path: '/login', handler: 'custom.login' },
    { method: 'POST', path: '/refresh', handler: 'custom.refresh' },
    { method: 'POST', path: '/logout', handler: 'custom.logout' },
  ],
};

Redémarrez Strapi, les points d’accès sont alors disponibles.

4. Implémentation côté client avec Next.js

Next.js peut appeler le point d’entrée de connexion, stocker le token d’accès dans un contexte React, et laisser le navigateur envoyer automatiquement le cookie de rafraîchissement.

tsx
// lib/auth.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
import axios from 'axios';

interface AuthContextProps {
  accessToken: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
}

const AuthContext = createContext<AuthContextProps | undefined>(undefined);

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [accessToken, setAccessToken] = useState<string | null>(null);

  const login = async (email: string, password: string) => {
    const res = await axios.post('/api/auth/login', { identifier: email, password });
    setAccessToken(res.data.accessToken);
  };

  const logout = async () => {
    await axios.post('/api/auth/logout');
    setAccessToken(null);
  };

  // Rafraîchissement automatique du token
  const refresh = async () => {
    try {
      const res = await axios.post('/api/auth/refresh');
      setAccessToken(res.data.accessToken);
    } catch {
      setAccessToken(null);
    }
  };

  // Rafraîchir le token toutes les 14 minutes (juste avant l’expiration du token d’accès)
  setInterval(refresh, 14 * 60 * 1000);

  return (
    <AuthContext.Provider value={{ accessToken, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
};

Pourquoi ne pas stocker le token dans un cookie ? Le placer dans un cookie l’exposerait aux attaques XSS. Le conserver en mémoire signifie qu’un rechargement de page le supprime, forçant le flux de rafraîchissement protégé par le cookie HttpOnly.

4.1 Ajout d’une protection CSRF (optionnel)

Si vous devez accepter des requêtes modifiant l’état qui ne sont pas couvertes par le flux JWT (par exemple des formulaires), activez le middleware CSRF de Strapi et transmettez le token dans un en‑tête :

js
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [{ key: 'X-CSRF-Token', value: process.env.CSRF_TOKEN }],
      },
    ];
  },
};

Côté serveur, ajoutez la configuration du plugin csrf intégré dans config/plugins.js :

js
module.exports = ({ env }) => ({
  csrf: {
    enabled: true,
    // Le token est envoyé dans un cookie nommé `csrf-token`
    cookie: { httpOnly: true, sameSite: 'strict', secure: true },
  },
});

Le client peut lire le cookie csrf-token (il n’est pas HttpOnly) et le placer dans l’en‑tête X-CSRF-Token pour les requêtes POST/PUT/DELETE.

Padlock and keys resting on a computer keyboard
Photo de Sasun Bughdaryan sur Unsplash

5. Renforcement du serveur Strapi

Au‑delà des drapeaux du cookie, appliquez les paramètres défensifs suivants :

ParamètreRecommandationOù le configurer
Rate limitingLimiter les tentatives de connexion (ex. 5 par minute par IP)Middleware personnalisé ou plugin tel que strapi-plugin-rate-limit
Hashage des mots de passeUtiliser Argon2 ou bcrypt avec un facteur de coût ≥12Par défaut dans Strapi, vérifiez la version de bcrypt
Journal d’auditEnregistrer les connexions réussies/échouées, les révocations de token/docs/recover-and-audit/audit – active la traçabilité
ConfidentialitéAnonymiser les adresses IP dans les journaux d’audit/docs/settings/privacy
Permissions des types de contenuRestreindre le rôle public aux seules actions find sur les points d’accès sûrsAdmin UI → Roles & Permissions

Activer le journal d’audit vous fournit une trace infalsifiable des événements d’authentification, utile pour la réponse aux incidents et la conformité RGPD.

6. Tester votre configuration

  1. Inspecter les cookies – Dans Chrome DevTools, vérifiez que le cookie refreshToken possède les attributs HttpOnly, Secure et SameSite=Strict.
  2. Tentative XSS – Essayez de lire document.cookie depuis une page contenant du script injecté ; le cookie de rafraîchissement doit rester invisible.
  3. Test CSRF – Depuis une autre origine, envoyez une requête POST sans l’en‑tête CSRF ; Strapi doit répondre 403.
  4. Rotation des tokens – Après un appel à /refresh, assurez‑vous que l’ancien refresh token n’est plus accepté (vous pouvez le vérifier avec Postman).

7. Déploiement en production

  • Assurez‑vous que votre load‑balancer termine le TLS avant de transmettre les requêtes à Strapi, tout en conservant le drapeau secure du cookie.
  • Utilisez des variables d’environnement pour les clés secrètes (JWT_SECRET, JWT_REFRESH_SECRET).
  • Définissez NODE_ENV=production afin que Strapi active les sécurités par défaut.
  • Faites tourner régulièrement les clés secrètes et invalidez les refresh tokens existants via un point d’accès administrateur.

8. Quand envisager Revisionary

Si vous avez besoin d’un enregistrement immuable des modifications liées à l’authentification (modifications de permissions, révocations de tokens), le journal d’audit Revisionary consigne ces actions aux côtés des éditions de contenu. Il offre un historique consultable pouvant soutenir les audits de conformité sans développement supplémentaire.

9. Checklist – sessions sécurisées pour Strapi + Next.js

  • Appliquer HttpOnly, Secure, SameSite=Strict au cookie de refresh‑token.
  • Conserver les access tokens en mémoire, jamais dans le localStorage ou les cookies.
  • Faire pivoter les refresh tokens à chaque utilisation.
  • Imposer une courte durée de vie aux access tokens (≤30 minutes).
  • Activer la protection CSRF pour les points d’accès modifiant l’état.
  • Ajouter un rate limiting sur les endpoints de connexion et de rafraîchissement.
  • Enregistrer les événements d’authentification dans le journal d’audit.
  • Anonymiser les IP dans les journaux d’audit pour la conformité vie privée.
  • Tester les drapeaux des cookies et la révocation des tokens après le déploiement.

Suivre ce guide vous procure une stratégie de session robuste, conforme aux recommandations OWASP, pour les applications headless CMS bâties avec Strapi et Next.js.

Mots-cléssécuritéauthentificationsessionscookiesStrapiNext.js

L’équipe Revisionary

Les développeurs de Revisionary, un plugin d’historique des versions pour Strapi 5. Nous écrivons sur Strapi, le développement web, l’IA et la sécurité des applications.

Cet article a été rédigé avec l'aide d'une IA.

Tous les articles