Les Tokens JWT Expliqués : Structure, Sécurité et Bonnes Pratiques

· 12 min de lecture

Les JSON Web Tokens (JWT) sont devenus la norme de facto pour l'authentification sans état dans les applications web modernes. Que vous construisiez une API REST, une architecture de microservices ou une application monopage, comprendre le fonctionnement des JWT est essentiel pour implémenter une authentification sécurisée et évolutive.

Ce guide complet détaille tout ce que vous devez savoir sur les JWT—de leur structure interne aux pratiques de sécurité prêtes pour la production. À la fin, vous comprendrez non seulement comment utiliser les JWT, mais quand les utiliser et comment éviter les pièges courants qui mènent à des vulnérabilités de sécurité.

Table des Matières

Qu'est-ce qu'un JWT ?

Un JSON Web Token (JWT) est un moyen compact et sécurisé pour les URL de représenter des revendications à transférer entre deux parties. Les revendications dans un JWT sont encodées sous forme d'objet JSON qui est signé numériquement à l'aide de JSON Web Signature (JWS).

Considérez un JWT comme un conteneur inviolable pour les informations utilisateur. Lorsqu'un utilisateur se connecte, votre serveur crée un JWT contenant son ID utilisateur, ses rôles et d'autres données pertinentes. Ce token est ensuite envoyé au client, qui l'inclut dans chaque requête ultérieure. Le serveur peut vérifier l'authenticité du token sans interroger une base de données, ce qui rend les JWT idéaux pour les architectures sans état et évolutives.

Les JWT sont définis par la RFC 7519 et sont largement pris en charge dans tous les langages de programmation et frameworks. Ils sont particulièrement populaires dans :

Conseil rapide : Utilisez notre Décodeur JWT pour inspecter et déboguer n'importe quel token JWT instantanément. C'est entièrement côté client, donc vos tokens ne quittent jamais votre navigateur.

Structure JWT : En-tête, Charge Utile et Signature

Un JWT se compose de trois parties encodées en Base64URL séparées par des points (.). Voici à quoi ressemble un vrai JWT :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Décomposition :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9           ← En-tête
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ  ← Charge utile
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c    ← Signature
Partie Contient Exemple (décodé)
En-tête Algorithme + type de token {"alg": "HS256", "typ": "JWT"}
Charge utile Revendications (données utilisateur) {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
Signature Hash de vérification HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

L'En-tête

L'en-tête se compose généralement de deux parties : le type de token (typ) et l'algorithme de signature (alg). Les algorithmes courants incluent :

La Charge Utile

La charge utile contient les revendications—des déclarations sur une entité (généralement l'utilisateur) et des métadonnées supplémentaires. Les revendications ne sont pas chiffrées, seulement encodées, donc ne mettez jamais d'informations sensibles comme des mots de passe dans une charge utile JWT.

La Signature

La signature garantit que le token n'a pas été altéré. Elle est créée en prenant l'en-tête encodé, la charge utile encodée, une clé secrète et en appliquant l'algorithme spécifié dans l'en-tête. Si quelqu'un modifie l'en-tête ou la charge utile, la vérification de la signature échouera.

Conseil pro : Générez des JWT de test avec des revendications personnalisées en utilisant notre outil Générateur JWT. Parfait pour les scénarios de développement et de test.

Revendications Standard (RFC 7519)

La RFC 7519 définit plusieurs revendications standard qui assurent l'interopérabilité entre différentes implémentations JWT. Bien que ces revendications soient optionnelles, leur utilisation correcte améliore la sécurité et la compatibilité.

Revendication Nom Objectif Exemple
iss Émetteur Identifie qui a créé le token "https://auth.example.com"
sub Sujet Identifie de qui parle le token (généralement l'ID utilisateur) "user_12345"
aud Audience Identifie à qui le token est destiné "https://api.example.com"
exp Expiration Quand le token expire (horodatage Unix) 1735689600
nbf Pas Avant Le token n'est pas valide avant cette heure 1735603200
iat Émis À Quand le token a été créé 1735603200
jti ID JWT Identifiant unique pour le token (utile pour la révocation) "a1b2c3d4-e5f6"

Revendications Personnalisées

Au-delà des revendications standard, vous pouvez ajouter des revendications personnalisées spécifiques à votre application. Les exemples courants incluent :

Gardez votre charge utile petite—chaque octet ajoute à la surcharge réseau. Incluez uniquement les revendications dont vous avez besoin pour les décisions d'autorisation. Si vous avez besoin de données utilisateur supplémentaires, récupérez-les depuis votre base de données en utilisant la revendication sub comme clé de recherche.

Comment Fonctionne l'Authentification JWT

Comprendre le flux d'authentification complet vous aide à implémenter les JWT correctement et en toute sécurité. Voici la séquence typique :

  1. Connexion Utilisateur : L'utilisateur envoie ses identifiants (nom d'utilisateur + mot de passe) à votre point de terminaison d'authentification
  2. Vérification des Identifiants : Le serveur valide les identifiants par rapport à votre base de données utilisateur
  3. Génération du Token : Le serveur crée un JWT contenant les revendications utilisateur et le signe avec une clé secrète
  4. Livraison du Token : Le serveur renvoie le JWT au client (généralement dans le corps de la réponse ou comme cookie httpOnly)
  5. Stockage du Token : Le client stocke le JWT en toute sécurité (plus d'informations dans la section Options de Stockage)
  6. Requêtes Authentifiées : Le client inclut le JWT dans l'en-tête Authorization pour les requêtes ultérieures
  7. Vérification du Token : Le serveur vérifie la signature et vérifie l'expiration—aucune recherche en base de données nécessaire
  8. Accès Accordé : Si valide, le serveur traite la requête en utilisant les revendications du token

Voici une représentation visuelle :

┌────────┐                                  ┌────────┐
│ Client │                                  │ Serveur│
└───┬────┘                                  └───┬────┘
    │                                           │
    │  POST /login {username, password}         │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [Vérifier identifiants]
    │                                    [Générer JWT]
    │                                           │
    │  200 OK {token: "eyJhbG..."}              │
    │<──────────────────────────────────────────│
    │                                           │
[Stocker token]                                 │
    │                                           │
    │  GET /api/profile                         │
    │  Authorization: Bearer eyJhbG...          │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [Vérifier signature]
    │                                    [Vérifier expiration]
    │                                    [Extraire revendications]
    │                                           │
    │  200 OK {user data}                       │
    │<──────────────────────────────────────────│
    │                                           │

Conseil pro : La beauté des JWT est que l'étape 7 (vérification du token) ne nécessite pas de requête en base de données. Le serveur peut vérifier l'authenticité et extraire les informations utilisateur directement depuis le token, rendant votre API hautement évolutive.

Exemples d'Implémentation

Examinons des implémentations pratiques dans les langages de programmation et frameworks populaires.

Node.js avec jsonwebtoken

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

// Point de terminaison de connexion
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Trouver l'utilisateur dans la base de données
  const user = await User.findOne({ username });
  if (!user) {
    return res.status(401).json({ error: 'Identifiants invalides' });
  }
  
  // Vérifier le mot de passe
  const validPassword = await bcrypt.compare(password, user.passwordHash);
  if (!validPassword) {
    return res.status(401).json({ error: 'Identifiants invalides' });
  }
  
  // Créer JWT
  const token = jwt.sign(
    { 
      sub: user.id,
      email: user.email,
      role: user.role 
    },
    process.env.JWT_SECRET,
    { 
      expiresIn: '15m',
      issuer: 'https://api.example.com',
      audience: 'https://example.com'
    }
  );
  
  res.json({ token });
});

// Middleware de route protégée
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  
  if (!token) {
    return res.status(401).json({ error: 'Aucun token fourni' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Token invalide ou expiré' });
  }
};

// Utiliser le middleware
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.sub, email: req.user.email });
});

Python avec PyJWT

import jwt
import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'votre-cle-secrete'

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # Vérifier les identifiants (simplifié)
    user = verify_credentials(username, password)
    if not user:
        return jsonify({'error': 'Identifiants invalides'}), 401
    
    # Créer JWT
    payload = {
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
        'iat': datetime.datetime.utcnow()
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return jsonify({'token': token})

def token_required(f):
    def decorator(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'Aucun token fourni'}), 401
        
        try:
            # Supprimer le préfixe 'Bearer '
            token = token.split(' ')[1]
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = decoded
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token expiré'}), 403
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Token invalide'}), 403
        
        return f(*args, **kwargs)
    
    return decorator

@app.route('/api/profile')
@token_required
def profile():
    return jsonify({'userId': request.user['sub']})

Go avec golang-jwt

package main

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID string `json:"sub"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func generateToken(userID, email, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  emai