🎫 Comprendre Complètement les Jetons JWT
📖 Définition
JWT (JSON Web Token) est un jeton basé sur JSON pour transmettre des informations en toute sécurité entre client et serveur. Il se compose de trois parties (Header, Payload, Signature), émis par le serveur et envoyé par le client lors des requêtes pour gérer l'authentification. Contrairement aux sessions, il ne stocke pas d'état sur le serveur (stateless), offrant ainsi une excellente évolutivité.
🎯 Comprendre avec une Analogie
Billet de Parc d'Attractions
En comparant JWT à un billet de parc d'attractions :
Billet Normal (Méthode Session)
├─ Recevoir un bracelet à l'entrée
├─ Le personnel vérifie le bracelet à chaque fois
└─ Le personnel doit se souvenir de tous les visiteurs (charge serveur)
Billet VIP (Méthode JWT)
├─ Billet avec sceau infalsifiable à l'entrée
├─ Le billet enregistre nom, validité, grade
├─ Montrer simplement le billet à chaque fois
└─ Le personnel vérifie uniquement le sceau (faible charge serveur)
JWT = Carte d'information avec sceau anti-contrefaçon
⚙️ Principe de Fonctionnement
1. Structure JWT
JWT = Header.Payload.Signature
Exemple :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJraW0ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
Header (En-tête) Payload (Charge utile) Signature
Header (En-tête)
{
"alg": "HS256", // Algorithme de signature
"typ": "JWT" // Type de jeton
}
// Encodé en Base64
Payload (Charge utile) - Données Réelles
{
"userId": 123,
"username": "kim",
"email": "kim@example.com",
"role": "admin",
"iat": 1640000000, // Issued At (Heure d'émission)
"exp": 1640086400 // Expiration (Heure d'expiration)
}
// Encodé en Base64
Signature - Prévention de Contrefaçon
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret // Clé secrète (seul le serveur la connaît)
)
2. Flux d'Authentification JWT
1. Connexion
Client → Serveur: username, password
Serveur: Authentification réussie → Génère et retourne JWT
2. Inclure JWT dans la Requête
Client → Serveur: Authorization: Bearer <JWT>
3. Serveur Vérifie JWT
- Vérifier la signature (si contrefait)
- Vérifier le délai d'expiration
- Extraire les informations utilisateur du Payload
4. Réponse
Serveur → Client: Données demandées
💡 Exemples Réels
Génération et Vérification JWT (Node.js)
const jwt = require('jsonwebtoken');
// Clé secrète (gestion avec variables d'environnement recommandée)
const SECRET_KEY = 'your-secret-key-keep-it-safe';
// ========== Génération JWT ==========
function generateToken(user) {
// Définir le Payload
const payload = {
userId: user.id,
username: user.username,
email: user.email,
role: user.role
};
// Options
const options = {
expiresIn: '1h', // Expire après 1 heure
// expiresIn: '7d', // 7 jours
// expiresIn: '30m', // 30 minutes
issuer: 'my-app', // Émetteur
subject: 'user-auth' // But
};
// Générer JWT
const token = jwt.sign(payload, SECRET_KEY, options);
return token;
}
// ========== Vérification JWT ==========
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
console.log('Vérification réussie:', decoded);
/*
{
userId: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin',
iat: 1640000000,
exp: 1640003600
}
*/
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.error('Jeton expiré');
} else if (error.name === 'JsonWebTokenError') {
console.error('Jeton invalide');
}
return null;
}
}
// ========== Exemple d'Utilisation ==========
const user = {
id: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin'
};
const token = generateToken(user);
console.log('JWT Généré:', token);
const decoded = verifyToken(token);
console.log('Décodé:', decoded);
Implémentation API de Connexion
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const users = []; // Utiliser une base de données en réalité
// ========== Inscription ==========
app.post('/api/register', async (req, res) => {
const { username, password, email } = req.body;
// Hash du mot de passe
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: users.length + 1,
username,
email,
password: hashedPassword
};
users.push(user);
res.json({ message: 'Inscription réussie', userId: user.id });
});
// ========== Connexion ==========
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// Rechercher l'utilisateur
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: 'Utilisateur non trouvé' });
}
// Vérifier le mot de passe
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Mot de passe incorrect' });
}
// Générer JWT
const token = jwt.sign(
{
userId: user.id,
username: user.username,
email: user.email
},
SECRET_KEY,
{ expiresIn: '1h' }
);
// Émettre aussi un Refresh Token (optionnel)
const refreshToken = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '7d' }
);
res.json({
message: 'Connexion réussie',
accessToken: token,
refreshToken: refreshToken
});
});
// ========== Middleware d'Authentification ==========
function authenticateToken(req, res, next) {
// Extraire le jeton de l'en-tête Authorization
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'Pas de jeton' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Jeton expiré' });
}
return res.status(403).json({ error: 'Jeton invalide' });
}
// Vérification réussie - sauvegarder les infos utilisateur dans req
req.user = user;
next();
});
}
// ========== Routes Protégées ==========
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user contient les informations extraites du JWT
res.json({
message: 'Informations du profil',
user: req.user
});
});
app.get('/api/admin', authenticateToken, (req, res) => {
// Vérification des permissions
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Permissions administrateur requises' });
}
res.json({ message: 'Page administrateur' });
});
app.listen(3000, () => {
console.log('Serveur en cours d\'exécution: http://localhost:3000');
});
Utilisation de JWT en Frontend
// ========== Connexion ==========
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
// Sauvegarder JWT dans LocalStorage
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
console.log('Connexion réussie!');
}
}
// ========== Inclure JWT dans les Requêtes API ==========
async function fetchProfile() {
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Jeton expiré → Aller à la page de connexion
window.location.href = '/login';
return;
}
const data = await response.json();
console.log('Profil:', data);
}
// ========== Automatisation avec Intercepteurs Axios ==========
import axios from 'axios';
// Intercepteur de requête: Ajouter JWT automatiquement à toutes les requêtes
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// Intercepteur de réponse: Déconnexion automatique en cas d'erreur 401
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Maintenant toutes les requêtes incluent JWT automatiquement!
axios.get('/api/profile').then(response => {
console.log(response.data);
});
Implémentation Refresh Token
// ========== Modèle Access Token + Refresh Token ==========
// Access Token: Durée courte (15 minutes)
// Refresh Token: Longue durée (7 jours)
app.post('/api/login', async (req, res) => {
// ... Vérification de connexion ...
const accessToken = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '15m' } // 15 minutes
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET_KEY,
{ expiresIn: '7d' } // 7 jours
);
// Sauvegarder Refresh Token en DB (optionnel)
// await saveRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken });
});
// ========== Renouveler Access Token ==========
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Pas de Refresh Token' });
}
jwt.verify(refreshToken, REFRESH_SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Refresh Token invalide' });
}
// Émettre nouveau Access Token
const accessToken = jwt.sign(
{ userId: user.userId },
SECRET_KEY,
{ expiresIn: '15m' }
);
res.json({ accessToken });
});
});
// ========== Frontend: Renouvellement Automatique de Jeton ==========
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Obtenir nouveau Access Token avec Refresh Token
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/refresh', { refreshToken });
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
// Réessayer la requête originale
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token aussi expiré → Déconnexion
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
🤔 Questions Fréquentes
Q1. Quelle est la différence entre JWT et Session ?
R:
// ========== Méthode Session ==========
// Serveur sauvegarde informations utilisateur
Connexion → Serveur sauvegarde session
{
sessionId: 'abc123',
userId: 123,
username: 'kim'
}
Client ne sauvegarde que sessionId
Cookie: sessionId=abc123
Requête avec sessionId pour consulter session serveur
Avantage: Serveur peut invalider immédiatement
Inconvénient: Besoin mémoire/stockage serveur, faible évolutivité
// ========== Méthode JWT ==========
// Client sauvegarde jeton
Connexion → Générer et retourner JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Client sauvegarde JWT entier
localStorage.setItem('token', jwt)
Requête inclut JWT, serveur vérifie uniquement signature
Avantage: Pas de charge serveur, bonne évolutivité
Inconvénient: Difficile invalider jeton, taille importante
Q2. JWT est-il sûr ?
R: Oui, s'il est utilisé correctement :
// ✅ Utilisation Sûre
1. Utiliser HTTPS obligatoire
- HTTP permet d'intercepter JWT
2. Sécurité de la clé secrète
- Gérer avec variables d'environnement
- Absolument interdit de coder en dur dans le code
const SECRET = process.env.JWT_SECRET;
3. Délai d'expiration court
- Access Token: 15 minutes~1 heure
- Renouveler avec Refresh Token
4. Interdiction de sauvegarder informations sensibles
❌ { password: '1234', ssn: '123-45-6789' }
✅ { userId: 123, role: 'user' }
5. Prévention XSS
- Cookie HttpOnly recommandé sur LocalStorage
res.cookie('token', jwt, {
httpOnly: true, // Pas d'accès JavaScript
secure: true, // HTTPS uniquement
sameSite: 'strict'
});
// ❌ Utilisation Dangereuse
1. Pas de délai d'expiration
2. Transmettre par HTTP
3. Sauvegarder dans LocalStorage (vulnérable XSS)
4. Inclure informations sensibles comme mot de passe
Q3. Comment invalider JWT ?
R: Il existe plusieurs méthodes :
// 1. Liste Noire (Blacklist)
// - Sauvegarder jeton en DB/Redis lors de la déconnexion
// - Vérifier liste noire lors de la requête
const blacklist = new Set();
app.post('/api/logout', authenticateToken, (req, res) => {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: 'Déconnexion réussie' });
});
function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (blacklist.has(token)) {
return res.status(401).json({ error: 'Jeton invalidé' });
}
// ... Vérification JWT ...
}
// 2. Sauvegarder dans Redis (configurer expiration)
const redis = require('redis');
const client = redis.createClient();
app.post('/api/logout', authenticateToken, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
// Maintenir en liste noire jusqu'à expiration du jeton
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await client.setEx(`blacklist:${token}`, ttl, 'true');
res.json({ message: 'Déconnexion réussie' });
});
// 3. Durée courte + Refresh Token
// - Access Token: 15 minutes (pas besoin d'invalidation)
// - Sauvegarder et invalider uniquement Refresh Token en DB
// 4. Gestion de Version
// - Sauvegarder version jeton utilisateur en DB
// - Augmenter version lors de la déconnexion
const payload = {
userId: user.id,
tokenVersion: user.tokenVersion // Obtenir de DB
};
// Déconnexion
await db.users.update(
{ id: userId },
{ $inc: { tokenVersion: 1 } } // Augmenter version
);
Q4. Où sauvegarder JWT ?
R:
// Option 1: LocalStorage (simple mais vulnérable XSS)
localStorage.setItem('token', jwt);
// ❌ Attaque XSS peut voler jeton
// Option 2: SessionStorage (supprimer à la fermeture onglet)
sessionStorage.setItem('token', jwt);
// ❌ Vulnérable XSS
// Option 3: Cookie HttpOnly (plus sûr, recommandé)
// Configurer sur serveur
res.cookie('token', jwt, {
httpOnly: true, // Pas d'accès JavaScript (prévenir XSS)
secure: true, // HTTPS uniquement
sameSite: 'strict',// Prévenir CSRF
maxAge: 3600000 // 1 heure
});
// Client envoie automatiquement
fetch('/api/profile', {
credentials: 'include' // Inclure Cookie
});
// Option 4: Mémoire (plus sûr mais déconnexion au rafraîchissement)
let token = null;
function login() {
// ... Connexion ...
token = response.accessToken; // Sauvegarder uniquement en variable
}
// Comparaison avantages et inconvénients
LocalStorage: Simple mais vulnérable XSS
Cookie: Sûr mais configuration complexe
Memory: Plus sûr mais mauvaise UX
Q5. Access Token vs Refresh Token ?
R:
// Access Token
// - Durée courte (15 minutes~1 heure)
// - Envoyer à chaque requête API
// - Expire rapidement même si volé
// Refresh Token
// - Longue durée (7 jours~30 jours)
// - Utiliser uniquement pour renouveler Access Token
// - Sauvegarder et gérer en DB
// Flux
1. Connexion
→ Émettre Access Token (15 min) + Refresh Token (7 jours)
2. Requête API
→ Utiliser Access Token
3. Access Token expiré (après 15 min)
→ Obtenir nouveau Access Token avec Refresh Token
4. Refresh Token aussi expiré (après 7 jours)
→ Nécessite reconnexion
// Exemple de code
// Émettre jetons
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
// Renouvellement automatique
if (accessTokenExpired) {
const newAccessToken = await refreshAccessToken(refreshToken);
}
🎓 Prochaines Étapes
Après avoir compris JWT, apprenez :
- Qu'est-ce que HTTPS ? (document en préparation) - Transmission sécurisée de jetons
- Qu'est-ce que CORS ? (document en préparation) - Sécurité API
- Qu'est-ce que Cookie ? - Session vs JWT
Outils Utiles
// Débogueur JWT
// https://jwt.io
// - Décodage et vérification JWT
// Bibliothèques
// Node.js: jsonwebtoken
// Python: PyJWT
// Java: jjwt
// Go: jwt-go
// Test
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const decoded = jwt.decode(token, { complete: true });
console.log(decoded);
/*
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { userId: 123, ... },
signature: '...'
}
*/
🎬 Résumé
JWT est la méthode d'authentification standard du web moderne :
- Structure: Header.Payload.Signature
- Avantages: Sans état, évolutivité, support multiplateforme
- Sécurité: HTTPS, Cookie HttpOnly, durée courte
- Modèle: Access Token + Refresh Token
Une utilisation correcte permet de construire un système d'authentification sûr et efficace ! 🎫✨