JWT (JSON Web Token) : guide complet pour sécuriser vos API et applications
Sécurité & APIJWTSécuritéAPIAuthentificationNode.js

JWT (JSON Web Token) : guide complet pour sécuriser vos API et applications

Découvrez JWT : fonctionnement, structure (header, payload, signature), avantages vs sessions, implémentation Node.js/React, bonnes pratiques de sécurité et cas d'usage.

JWT (JSON Web Token) est devenu le standard de facto pour l'authentification et l'autorisation dans les applications web modernes et les API RESTful. Compact, sécurisé et stateless, JWT permet de transmettre des informations de manière fiable entre parties tout en garantissant leur intégrité. Ce guide explore en profondeur le fonctionnement de JWT, ses avantages, son implémentation pratique et les bonnes pratiques de sécurité indispensables.

Qu'est-ce que JWT (JSON Web Token) ?

JWT est un standard ouvert (RFC 7519) qui définit une méthode compacte et autonome pour transmettre de manière sécurisée des informations entre parties sous forme d'objet JSON. Ces informations peuvent être vérifiées et approuvées car elles sont signées numériquement.

Caractéristiques principales

  • Compact : taille réduite, facile à transmettre via URL, header HTTP ou body
  • Autonome (self-contained) : contient toutes les informations nécessaires sur l'utilisateur, évitant des requêtes supplémentaires en base de données
  • Signé numériquement : garantit l'intégrité et l'authenticité des données (signature HMAC ou RSA)
  • Stateless : le serveur n'a pas besoin de stocker les sessions, facilitant la scalabilité

Structure d'un JWT

Un JWT se compose de trois parties séparées par des points (.) : header.payload.signature

1. Header (En-tête)

Contient le type de token (JWT) et l'algorithme de signature utilisé (HS256, RS256, etc.).

{
  "alg": "HS256",
  "typ": "JWT"
}

Le header est encodé en Base64URL pour former la première partie du token.

2. Payload (Charge utile)

Contient les revendications (claims) : informations sur l'utilisateur et métadonnées. Il existe trois types de claims :

Claims enregistrés (Registered Claims)

  • iss (issuer) : émetteur du token
  • sub (subject) : sujet du token (généralement l'ID utilisateur)
  • aud (audience) : destinataire du token
  • exp (expiration time) : date d'expiration (timestamp Unix)
  • nbf (not before) : date avant laquelle le token n'est pas valide
  • iat (issued at) : date d'émission
  • jti (JWT ID) : identifiant unique du token

Claims publics

Définis par ceux qui utilisent JWT. Doivent être enregistrés dans l'IANA JSON Web Token Registry ou définis avec un namespace pour éviter les collisions.

Claims privés

Claims custom définis entre les parties qui utilisent le token (email, role, permissions, etc.).

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Le payload est encodé en Base64URL pour former la deuxième partie du token.

3. Signature

Garantit que le token n'a pas été modifié. La signature est créée en combinant le header encodé, le payload encodé et une clé secrète (ou une paire de clés publique/privée pour RSA).

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Token JWT complet

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Comment fonctionne JWT ?

Flux d'authentification typique

  1. Login : L'utilisateur envoie ses identifiants (email/password) au serveur
  2. Vérification : Le serveur vérifie les identifiants en base de données
  3. Génération JWT : Si les identifiants sont valides, le serveur génère un JWT contenant l'ID utilisateur, le rôle, les permissions, etc.
  4. Retour du token : Le serveur retourne le JWT au client
  5. Stockage client : Le client stocke le JWT (localStorage, sessionStorage, cookie httpOnly)
  6. Requêtes authentifiées : Pour chaque requête nécessitant une authentification, le client envoie le JWT dans le header Authorization: Bearer <token>
  7. Vérification serveur : Le serveur vérifie la signature du JWT et décode le payload pour identifier l'utilisateur
  8. Autorisation : Le serveur accorde ou refuse l'accès en fonction des informations contenues dans le JWT (rôle, permissions)

JWT vs Sessions : quelle différence ?

Authentification par session (traditionnelle)

  • Stateful : le serveur stocke les sessions en mémoire, en base de données ou en cache (Redis)
  • Cookie de session : un identifiant de session est stocké dans un cookie côté client
  • Requête base de données : à chaque requête, le serveur doit interroger le store de sessions pour valider la session
  • Scalabilité limitée : complexité pour partager les sessions entre plusieurs serveurs (sticky sessions ou session store centralisé)

Authentification par JWT

  • Stateless : le serveur ne stocke rien, toutes les informations sont dans le token
  • Token JWT : transmis via header Authorization ou cookie
  • Pas de requête base de données : le serveur vérifie uniquement la signature du token
  • Scalabilité optimale : n'importe quel serveur peut valider le token sans coordination

Tableau comparatif

Critère Session JWT
Stockage serveur Oui (stateful) Non (stateless)
Scalabilité Complexe (sticky sessions) Simple (aucune coordination)
Performance Requête store sessions Vérification signature uniquement
Révocation Immédiate (suppression session) Complexe (blacklist ou expiration courte)
Taille Cookie léger (ID session) Token plus volumineux (données + signature)
Multi-domaines Complexe (CORS, sous-domaines) Simple (header Authorization)

Implémentation JWT avec Node.js

Installation des dépendances

npm install jsonwebtoken bcryptjs express dotenv

Génération d'un JWT (login)

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

// Route de login
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  // 1. Récupérer l'utilisateur en base de données
  const user = await User.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Identifiants invalides' });
  }

  // 2. Vérifier le mot de passe
  const isPasswordValid = await bcrypt.compare(password, user.password);
  if (!isPasswordValid) {
    return res.status(401).json({ error: 'Identifiants invalides' });
  }

  // 3. Générer le JWT
  const token = jwt.sign(
    {
      userId: user._id,
      email: user.email,
      role: user.role,
    },
    process.env.JWT_SECRET, // Clé secrète stockée en variable d'environnement
    { expiresIn: '24h' } // Expiration après 24 heures
  );

  // 4. Retourner le token
  res.json({
    token,
    user: {
      id: user._id,
      email: user.email,
      name: user.name,
      role: user.role,
    },
  });
});

Middleware de vérification JWT

const jwt = require('jsonwebtoken');

const authMiddleware = (req, res, next) => {
  // 1. Récupérer le token depuis le header Authorization
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token manquant' });
  }

  const token = authHeader.split(' ')[1];

  try {
    // 2. Vérifier et décoder le token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // 3. Attacher les informations utilisateur à la requête
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expiré' });
    }
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({ error: 'Token invalide' });
    }
    return res.status(500).json({ error: 'Erreur serveur' });
  }
};

// Utilisation sur une route protégée
app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({
    userId: req.user.userId,
    email: req.user.email,
    role: req.user.role,
  });
});

Refresh token pattern

Pour améliorer la sécurité, utiliser deux tokens :

  • Access token : courte durée de vie (15 min), utilisé pour les requêtes API
  • Refresh token : longue durée de vie (7 jours), stocké en base de données, utilisé pour renouveler l'access token
// Génération des deux tokens
const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' });

// Stocker le refresh token en base de données
await RefreshToken.create({
  userId: user._id,
  token: refreshToken,
  expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});

// Route de refresh
app.post('/api/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  // Vérifier que le refresh token existe en base
  const storedToken = await RefreshToken.findOne({ token: refreshToken });
  if (!storedToken) {
    return res.status(401).json({ error: 'Refresh token invalide' });
  }

  try {
    // Vérifier la signature du refresh token
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);

    // Générer un nouvel access token
    const newAccessToken = jwt.sign(
      { userId: decoded.userId, email: decoded.email, role: decoded.role },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (error) {
    return res.status(401).json({ error: 'Refresh token expiré ou invalide' });
  }
});

Implémentation JWT côté client (React)

Stockage du token

// Option 1 : localStorage (simple mais vulnérable XSS)
localStorage.setItem('token', token);

// Option 2 : sessionStorage (supprimé à la fermeture du navigateur)
sessionStorage.setItem('token', token);

// Option 3 : cookie httpOnly (recommandé, non accessible en JavaScript)
// Le serveur définit le cookie avec l'attribut httpOnly
res.cookie('token', token, {
  httpOnly: true,
  secure: true, // HTTPS uniquement
  sameSite: 'strict',
  maxAge: 24 * 60 * 60 * 1000, // 24 heures
});

Envoi du token avec Axios

import axios from 'axios';

// Interceptor pour ajouter automatiquement le token à chaque requête
axios.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Interceptor pour gérer l'expiration du token
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        // Tenter de renouveler le token avec le refresh token
        const refreshToken = localStorage.getItem('refreshToken');
        const { data } = await axios.post('/api/auth/refresh', { refreshToken });

        localStorage.setItem('token', data.accessToken);
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;

        return axios(originalRequest);
      } catch (refreshError) {
        // Refresh token invalide ou expiré : rediriger vers login
        localStorage.removeItem('token');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Bonnes pratiques de sécurité JWT

1. Utiliser des secrets forts

La clé secrète JWT doit être longue (minimum 256 bits), aléatoire et stockée en variable d'environnement. Ne jamais la commiter dans Git.

# .env
JWT_SECRET=a8f5d2c7b9e4f1a3d6c8b5e2a7f4d1c9b6e3a8f5d2c7b9e4f1a3d6c8b5e2a7f4

2. Définir une durée d'expiration courte

Access token : 15 minutes à 1 heure maximum. Utiliser un refresh token pour renouveler l'accès sans redemander les identifiants.

3. Ne pas stocker d'informations sensibles dans le payload

Le payload JWT est seulement encodé en Base64, pas chiffré. N'y stockez jamais de mots de passe, numéros de carte bancaire ou données personnelles sensibles.

4. Utiliser HTTPS

Toujours transmettre les JWT sur des connexions HTTPS pour éviter l'interception par des attaquants (man-in-the-middle).

5. Implémenter une blacklist pour la révocation

Bien que JWT soit stateless, il est parfois nécessaire de révoquer un token (déconnexion, changement de mot de passe). Solutions :

  • Blacklist en cache : stocker les tokens révoqués en Redis avec expiration
  • Versionning : inclure une version dans le token et l'incrémenter lors d'une révocation
  • Expiration courte + refresh token : faciliter la révocation en invalidant le refresh token en base

6. Protéger contre les attaques XSS

Si le token est stocké en localStorage/sessionStorage, il est vulnérable aux attaques XSS. Privilégier le stockage en cookie httpOnly (non accessible en JavaScript).

7. Protéger contre les attaques CSRF

Si le token est stocké en cookie, implémenter une protection CSRF (token CSRF, attribut SameSite=Strict).

8. Valider le token à chaque requête

Ne jamais faire confiance au client. Toujours vérifier la signature du JWT côté serveur avant d'autoriser l'accès.

9. Utiliser l'algorithme RSA pour les architectures distribuées

Pour les microservices, privilégier RSA (RS256) : le serveur d'authentification signe avec la clé privée, les autres services vérifient avec la clé publique (pas besoin de partager un secret).

Cas d'usage de JWT

1. Authentification API RESTful

JWT est idéal pour sécuriser les API stateless : le client envoie le token à chaque requête, le serveur vérifie et autorise l'accès.

2. Single Sign-On (SSO)

JWT permet l'authentification unique sur plusieurs domaines : un serveur d'authentification central émet un JWT accepté par tous les services.

3. Échange d'informations sécurisé

JWT garantit l'intégrité des données : si un tiers modifie le payload, la signature ne correspondra plus et le token sera rejeté.

4. Autorisation fine (RBAC, ABAC)

Le JWT peut contenir les rôles et permissions de l'utilisateur, permettant une autorisation côté serveur sans requête base de données.

{
  "userId": "123",
  "role": "admin",
  "permissions": ["users:read", "users:write", "users:delete"]
}

5. Authentification mobile et IoT

Les applications mobiles et objets connectés utilisent JWT pour éviter de retransmettre les identifiants à chaque requête.

Alternatives et compléments à JWT

OAuth 2.0

Protocole d'autorisation permettant à une application tierce d'accéder aux ressources d'un utilisateur sans connaître son mot de passe. JWT est souvent utilisé comme format de token dans OAuth 2.0 (access token, ID token).

OpenID Connect (OIDC)

Couche d'identité au-dessus d'OAuth 2.0, utilisant JWT pour l'ID token qui contient les informations d'identité de l'utilisateur.

PASETO (Platform-Agnostic Security Tokens)

Alternative à JWT conçue pour éviter certaines vulnérabilités (choix d'algorithme faible, algorithme "none"). PASETO impose des algorithmes sécurisés par défaut.

Macaroons

Tokens de capacité (capability tokens) permettant une délégation fine des permissions. Plus complexes que JWT mais offrent plus de flexibilité pour l'atténuation des droits.

Erreurs courantes à éviter

1. Utiliser l'algorithme "none"

Certaines bibliothèques acceptent un JWT sans signature (alg: "none"). Toujours valider l'algorithme attendu côté serveur.

2. Stocker des données sensibles dans le payload

Le payload est visible par quiconque possède le token (décodage Base64). Ne jamais y mettre de mots de passe ou données confidentielles.

3. Ne pas définir d'expiration

Un JWT sans expiration (claim exp) est valide indéfiniment. Toujours définir une durée de vie.

4. Réutiliser la même clé secrète partout

Utiliser des clés différentes pour l'access token et le refresh token. En cas de compromission d'une clé, l'autre reste sécurisée.

5. Ne pas valider les claims

Toujours vérifier iss (émetteur), aud (audience), exp (expiration) pour s'assurer que le token est destiné à votre application.

Pourquoi choisir VOID pour votre authentification JWT ?

Expertise sécurité

Nous implémentons JWT selon les meilleures pratiques de sécurité : algorithmes robustes (HS256, RS256), expiration courte, refresh tokens, protection XSS/CSRF.

Architecture scalable

Nos architectures stateless JWT permettent de scaler horizontalement sans complexité : n'importe quel serveur peut valider un token sans coordination.

Intégrations OAuth/OIDC

Nous intégrons JWT avec OAuth 2.0 et OpenID Connect pour l'authentification via Google, Facebook, Azure AD, permettant le SSO et l'authentification déléguée.

Monitoring et audit

Nous instrumentons les flux d'authentification : logs centralisés, alertes sur tentatives de connexion suspectes, dashboards de sécurité temps réel.

Conformité réglementaire

Nos implémentations JWT respectent les standards de sécurité (OWASP, ISO 27001) et les réglementations sectorielles (PCI-DSS pour le paiement, loi 09-08 pour les données personnelles au Maroc).

Conclusion

JWT s'est imposé comme le standard de l'authentification moderne grâce à sa simplicité, sa scalabilité et sa sécurité. Bien implémenté, JWT offre une solution élégante pour sécuriser vos API et applications tout en facilitant l'expérience utilisateur. Chez VOID, nous maîtrisons l'intégralité de la chaîne : de l'architecture sécurisée à l'implémentation backend/frontend, du monitoring à la conformité réglementaire. Que vous construisiez une API RESTful, une architecture microservices ou une application mobile, nous concevons des systèmes d'authentification robustes, performants et évolutifs basés sur JWT.

Besoin d'accompagnement ?

Notre équipe d'experts peut vous aider à mettre en œuvre ces recommandations.

Contactez-nous