Contactez-nous

Utilisation de JWT (JSON Web Tokens) pour l'authentification

Apprenez a implementer l'authentification stateless en Node.js avec les JSON Web Tokens (JWT). Creez, signez, verifiez des tokens pour securiser vos API.

Introduction : l'authentification Stateless avec JWT

Alors que les sessions offrent une méthode d'authentification stateful (le serveur stocke l'état de la session), une approche alternative très populaire, en particulier pour les API RESTful, les applications monopages (SPA) et les architectures microservices, est l'authentification stateless basée sur les JSON Web Tokens (JWT).

Un JWT est un standard ouvert (RFC 7519) qui définit une manière compacte et autonome de transmettre en toute sécurité des informations (appelées "claims" ou revendications) entre parties sous forme d'un objet JSON. L'information est "signée numériquement", ce qui permet au destinataire de vérifier son authenticité et son intégrité. Le principal avantage est que le serveur n'a pas besoin de maintenir un enregistrement de session pour l'utilisateur ; toutes les informations nécessaires (ou un minimum d'informations identifiantes) sont contenues dans le token lui-même.

Le flux général est le suivant : l'utilisateur se connecte, le serveur vérifie les identifiants et, si valides, génère un JWT contenant des informations sur l'utilisateur (comme son ID) et le signe avec une clé secrète. Ce token est renvoyé au client, qui le stocke et l'envoie avec chaque requête ultérieure vers des ressources protégées (généralement dans l'en-tête `Authorization`). Le serveur vérifie alors la signature du token pour authentifier la requête sans avoir besoin de consulter une base de données de sessions.

Anatomie d'un JSON Web Token

Un JWT se compose de trois parties séparées par des points (`.`), chacune encodée en Base64URL :

  1. Header (En-tête) : Contient des métadonnées sur le token, typiquement le type de token (JWT) et l'algorithme de signature utilisé (ex: HS256 - HMAC avec SHA-256, ou RS256 - RSA).
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  2. Payload (Charge Utile) : Contient les "claims" (revendications), qui sont des informations sur l'entité (généralement l'utilisateur) et des métadonnées supplémentaires. Il existe des claims enregistrés (standards comme `iss` - émetteur, `sub` - sujet/ID utilisateur, `exp` - date d'expiration, `iat` - émis à), des claims publics, et des claims privés (spécifiques à l'application). Important : Le payload est seulement encodé en Base64URL, pas chiffré ! N'y stockez pas d'informations sensibles non protégées. Toute personne qui intercepte le token peut lire le payload. Sa sécurité réside dans la vérification de la signature.
    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true,
      "iat": 1516239022,
      "exp": 1516242622 
    }
  3. Signature : Pour créer la signature, vous prenez l'en-tête encodé, la charge utile encodée, vous les concaténez avec un point, et vous signez cette chaîne avec l'algorithme spécifié dans l'en-tête et une clé secrète (pour HS256) ou une clé privée (pour RS256). Cette signature garantit que le token n'a pas été modifié et, dans le cas de HS256, qu'il a bien été émis par la partie qui connaît le secret.

Le token final ressemble à quelque chose comme `xxxxx.yyyyy.zzzzz`.

Implementation avec Node.js et `jsonwebtoken`

La bibliothèque la plus populaire pour travailler avec les JWT en Node.js est `jsonwebtoken`. Installez-la :

npm install jsonwebtoken

1. Création et signature du token (Login) : Après avoir vérifié les identifiants de l'utilisateur, vous créez le token avec `jwt.sign()`.

const jwt = require('jsonwebtoken');

// !! Gardez ce secret en sécurité et hors du code (variable d'env) !!
const JWT_SECRET = process.env.JWT_SECRET || 'votre_super_secret_complexe';

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  // ... (Vérifiez username et password contre la BDD avec bcrypt.compare)
  const user = await authenticateUser(username, password); // Fonction fictive

  if (!user) {
    return res.status(401).json({ message: 'Authentification échouée' });
  }

  // Créer le payload du token (ne mettez que l'essentiel, pas de données sensibles)
  const payload = {
    userId: user.id,
    username: user.username,
    role: user.role
    // Ajoutez d'autres claims si nécessaire (iss, aud, etc.)
  };

  // Signer le token
  try {
    const token = jwt.sign(
      payload,
      JWT_SECRET,
      { expiresIn: '1h' } // Options: durée de validité (ex: 1 heure)
      // { algorithm: 'RS256' } // Si vous utilisez des clés asymétriques
    );

    // Envoyer le token au client
    res.json({ message: 'Connexion réussie', token: token });

  } catch (error) {
      console.error('Erreur lors de la signature du JWT:', error);
      res.status(500).json({ message: 'Erreur interne lors de la création du token' });
  }
});

2. Vérification du token (Middleware) : Pour protéger les routes, vous créez un middleware qui extrait le token de l'en-tête `Authorization` et le vérifie avec `jwt.verify()`.

function authenticateToken(req, res, next) {
  // Récupérer le token depuis l'en-tête Authorization
  // Format attendu: Bearer 
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (token == null) {
    return res.sendStatus(401); // Pas de token fourni
  }

  // Vérifier le token
  jwt.verify(token, JWT_SECRET, (err, decodedPayload) => {
    if (err) {
      console.log('Erreur de vérification JWT:', err.message); // Ex: 'jwt expired', 'invalid signature'
      // Si le token est expiré ou invalide
      return res.sendStatus(403); // Forbidden (ou 401 selon votre stratégie)
    }

    // Le token est valide, le payload décodé est dans 'decodedPayload'
    // Attacher les informations utilisateur à la requête pour les routes suivantes
    req.user = decodedPayload;
    console.log('Token vérifié, utilisateur:', req.user.username);
    next(); // Passer à la route protégée
  });
}

// Utiliser le middleware pour protéger une route
app.get('/api/profile', authenticateToken, (req, res) => {
  // Seuls les utilisateurs avec un token valide atteignent ce point
  // req.user contient les informations du payload (userId, username, role)
  res.json({ 
    message: 'Ceci est votre profil protégé.',
    user: req.user 
  });
});

Ce middleware est la pierre angulaire de la protection de vos API basées sur JWT.

Stockage du token cote client

Une fois le token reçu après le login, le client doit le stocker pour pouvoir le renvoyer dans les requêtes suivantes. Plusieurs options existent, chacune avec ses compromis de sécurité :

  • `localStorage` / `sessionStorage` : Facile à utiliser, mais vulnérable aux attaques XSS (Cross-Site Scripting). Si un script malveillant s'exécute sur votre site, il peut lire et voler le token depuis le `localStorage`.
  • Cookie `HttpOnly` : Plus sécurisé contre XSS car le cookie n'est pas accessible par JavaScript côté client. Cependant, il peut être vulnérable aux attaques CSRF (Cross-Site Request Forgery) si des mesures de protection supplémentaires (comme les tokens CSRF ou l'attribut `SameSite` du cookie) ne sont pas mises en place. Si vous utilisez cette méthode, le serveur doit aussi être configuré pour lire le token depuis le cookie au lieu de l'en-tête `Authorization`.

Le choix dépend du contexte et du niveau de sécurité requis. Pour les SPA, l'envoi via l'en-tête `Authorization` depuis `localStorage` (en prenant des mesures anti-XSS robustes) ou l'utilisation de cookies `HttpOnly` avec `SameSite=Lax` ou `Strict` sont des approches courantes.

Avantages et inconvenients des JWT

Avantages :

  • Stateless : Le serveur n'a pas besoin de stocker d'état de session, ce qui simplifie la scalabilité horizontale (ajout facile de serveurs sans se soucier de la synchronisation des sessions).
  • Découplage : Idéal pour les architectures découplées (microservices, API pour mobiles/SPA) car le token est autonome.
  • Performance : Pas de lookup en base de données de session à chaque requête (la vérification est cryptographique).
  • Portabilité : Standardisé et utilisable entre différents domaines ou services.

Inconvénients :

  • Payload lisible : Les données du payload ne sont qu'encodées, pas chiffrées. Ne pas y stocker d'informations sensibles.
  • Révocation difficile : Une fois émis, un JWT est valide jusqu'à son expiration. Il est complexe de révoquer un token spécifique avant sa date d'expiration sans maintenir une liste noire côté serveur (ce qui réintroduit un état). Des stratégies comme des durées de vie courtes et des refresh tokens sont souvent utilisées pour mitiger cela.
  • Taille du token : Si le payload contient beaucoup d'informations, le token peut devenir volumineux, augmentant la taille des en-têtes de requête.
  • Gestion du secret/clé : La sécurité du secret (HS256) ou de la clé privée (RS256) est absolument critique. Une compromission permettrait de forger des tokens valides.

En conclusion, les JWT offrent une méthode d'authentification stateless puissante et flexible, particulièrement adaptée aux API modernes et aux architectures distribuées. Cependant, il est crucial de comprendre leur fonctionnement, leurs implications de sécurité (stockage client, secret/clé, révocation) et d'implémenter la vérification de manière robuste côté serveur.