Objectif: comprendre les fondations REST/JWT avant de coder une API sécurisée.
Fonctionnement: valide chaque concept avec un cas concret, puis relie-le aux parties implémentation PHP/Python.
REST est un style d'architecture pour les systèmes distribués, défini par Roy Fielding en 2000. Une API REST utilise HTTP de manière standardisée pour exposer des ressources.
Cache-Control, ETag, Expires| Méthode | Description | Idempotent | Safe | Exemple |
|---|---|---|---|---|
GET |
Récupérer une ressource | ✅ | ✅ | GET /users/123 |
POST |
Créer une ressource | ❌ | ❌ | POST /users |
PUT |
Remplacer complètement une ressource | ✅ | ❌ | PUT /users/123 |
PATCH |
Modifier partiellement une ressource | ❌ | ❌ | PATCH /users/123 |
DELETE |
Supprimer une ressource | ✅ | ❌ | DELETE /users/123 |
HEAD |
Comme GET sans le body | ✅ | ✅ | HEAD /users/123 |
OPTIONS |
Obtenir les méthodes supportées | ✅ | ✅ | OPTIONS /users |
200 OK → Requête réussie (GET, PUT, PATCH) 201 Created → Ressource créée (POST) 204 No Content → Succès sans contenu (DELETE)
301 Moved Permanently → Ressource déplacée définitivement 302 Found → Redirection temporaire 304 Not Modified → Cache valide (utiliser la version locale)
400 Bad Request → Syntaxe invalide 401 Unauthorized → Authentification requise 403 Forbidden → Authentifié mais pas autorisé 404 Not Found → Ressource inexistante 405 Method Not Allowed → Méthode HTTP non supportée 409 Conflict → Conflit (ex: email déjà utilisé) 422 Unprocessable Entity → Validation échouée 429 Too Many Requests → Rate limiting dépassé
500 Internal Server Error → Erreur générale du serveur 502 Bad Gateway → Proxy/Gateway invalide 503 Service Unavailable → Serveur surchargé ou maintenance 504 Gateway Timeout → Timeout du proxy/gateway
# CRUD sur les utilisateurs GET /api/users → Liste tous les utilisateurs GET /api/users/123 → Récupère l'utilisateur #123 POST /api/users → Crée un nouvel utilisateur PUT /api/users/123 → Remplace l'utilisateur #123 PATCH /api/users/123 → Modifie l'utilisateur #123 DELETE /api/users/123 → Supprime l'utilisateur #123 # Ressources imbriquées GET /api/users/123/posts → Posts de l'utilisateur #123 POST /api/users/123/posts → Créer un post pour l'utilisateur #123 GET /api/users/123/posts/456 → Post #456 de l'utilisateur #123 DELETE /api/users/123/posts/456 → Supprimer le post #456 # Actions spécifiques (éviter si possible) POST /api/users/123/activate → Activer l'utilisateur POST /api/users/123/reset-password → Réinitialiser le mot de passe
/users pas /user/users pas /getUsers/user-profiles/api ou un numéro de version : /api/v1/usersPOST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"username": "john_doe",
"email": "john@example.com",
"password": "SecurePass123!",
"profile": {
"firstName": "John",
"lastName": "Doe",
"age": 30
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/123
{
"id": 123,
"username": "john_doe",
"email": "john@example.com",
"profile": {
"firstName": "John",
"lastName": "Doe",
"age": 30
},
"createdAt": "2025-11-30T10:30:00Z",
"updatedAt": "2025-11-30T10:30:00Z",
"_links": {
"self": "/api/users/123",
"posts": "/api/users/123/posts"
}
}
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Les données fournies sont invalides",
"details": [
{
"field": "email",
"message": "Format d'email invalide"
},
{
"field": "password",
"message": "Le mot de passe doit contenir au moins 8 caractères"
}
]
}
}
GET /api/users?page=2&limit=20
{
"data": [
{ "id": 21, "username": "user21" },
{ "id": 22, "username": "user22" }
// ... 20 résultats
],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
},
"_links": {
"first": "/api/users?page=1&limit=20",
"prev": "/api/users?page=1&limit=20",
"self": "/api/users?page=2&limit=20",
"next": "/api/users?page=3&limit=20",
"last": "/api/users?page=8&limit=20"
}
}
JWT est un standard ouvert (RFC 7519) pour créer des tokens d'accès sécurisés. C'est une chaîne encodée contenant des informations (claims) signées cryptographiquement.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c │─────────── Header ──────────│─────────────── Payload ────────────────│────────── Signature ─────────│
{
"alg": "HS256", // Algorithme de signature (HMAC SHA-256)
"typ": "JWT" // Type de token
}
{
// Claims standards (RFC 7519)
"iss": "https://api.example.com", // Issuer (émetteur)
"sub": "123", // Subject (utilisateur)
"aud": "https://app.example.com", // Audience (destinataire)
"exp": 1735560000, // Expiration (timestamp)
"nbf": 1735559000, // Not Before (pas avant)
"iat": 1735559000, // Issued At (émis à)
"jti": "abc123", // JWT ID (identifiant unique)
// Claims personnalisés
"username": "john_doe",
"email": "john@example.com",
"roles": ["user", "admin"]
}
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret_key )
La signature garantit que :
┌─────────┐ ┌─────────┐
│ Client │ │ Serveur │
└────┬────┘ └────┬────┘
│ │
│ 1. POST /api/auth/login │
│ { username, password } │
├──────────────────────────────────────────────────►│
│ │
│ 2. Vérifier credentials │
│ Générer JWT │
│ │
│ 3. 200 OK │
│ { token: "eyJ...", refreshToken: "..." } │
│◄──────────────────────────────────────────────────┤
│ │
│ Stocker le token (localStorage / cookie) │
│ │
│ 4. GET /api/users/me │
│ Authorization: Bearer eyJ... │
├──────────────────────────────────────────────────►│
│ │
│ 5. Vérifier signature │
│ Décoder payload │
│ Vérifier expiration │
│ │
│ 6. 200 OK │
│ { id, username, email, ... } │
│◄──────────────────────────────────────────────────┤
│ │
│ ... Le token expire après 15 minutes ... │
│ │
│ 7. GET /api/users/me │
│ Authorization: Bearer eyJ... (expiré) │
├──────────────────────────────────────────────────►│
│ │
│ 8. 401 Unauthorized │
│ { error: "Token expired" } │
│◄──────────────────────────────────────────────────┤
│ │
│ 9. POST /api/auth/refresh │
│ { refreshToken: "..." } │
├──────────────────────────────────────────────────►│
│ │
│ 10. Vérifier refresh │
│ Générer nouveau JWT │
│ │
│ 11. 200 OK │
│ { token: "eyJ... (nouveau)" } │
│◄──────────────────────────────────────────────────┤
│ │
| Aspect | Access Token | Refresh Token |
|---|---|---|
| Durée de vie | Courte (15 min - 1h) | Longue (7 jours - 30 jours) |
| Usage | Accéder aux ressources protégées | Obtenir un nouveau access token |
| Envoi | À chaque requête (header Authorization) | Uniquement pour le refresh |
| Stockage | Memory / sessionStorage (SPA) | httpOnly cookie (sécurisé) |
| Révocation | Difficile (attendre expiration) | Possible (stocké en DB) |
| Risque XSS | Élevé si dans localStorage | Faible si httpOnly cookie |
✅ RECOMMANDÉ HS256 (HMAC SHA-256) → Secret symétrique, simple RS256 (RSA SHA-256) → Clés asymétriques, plus sécurisé pour microservices ES256 (ECDSA SHA-256) → Clés asymétriques, signatures plus courtes ❌ À ÉVITER none → Pas de signature (vulnérable !) HS512 avec clé faible → Force brute possible
❌ NE PAS FAIRE localStorage → Vulnérable aux attaques XSS Cookie sans httpOnly → Accessible en JavaScript ✅ RECOMMANDÉ Memory (variable JS) → Perdu au refresh mais sécurisé sessionStorage → Compromis acceptable pour SPA httpOnly Cookie → Le plus sécurisé (pas accessible en JS)
# Générer un secret robuste (256 bits minimum) openssl rand -base64 32 # Exemple de secret fort JWT_SECRET=3F2504E0-4F89-11D3-9A0C-0305E82C3301-8A8F6B9C2D3E4F5A6B7C8D9E0F1A2B3C
À vérifier côté serveur : ✓ Signature valide (avec la bonne clé) ✓ Token non expiré (exp) ✓ Token pas encore valide (nbf) ✓ Issuer correct (iss) ✓ Audience correcte (aud) ✓ Token ID pas en blacklist (jti)
✅ TOUJOURS utiliser HTTPS en production ❌ JAMAIS envoyer un JWT en HTTP (interception possible)
❌ NE PAS mettre dans le JWT
{
"password": "...", // JAMAIS de mot de passe
"creditCard": "...", // JAMAIS de données bancaires
"ssn": "..." // JAMAIS de données personnelles sensibles
}
✅ Stocker uniquement
{
"userId": 123,
"username": "john_doe",
"roles": ["user"],
"email": "john@example.com" // OK car pas ultra-sensible
}
# Access token : 15 minutes à 1 heure maximum exp: Date.now() + 15 * 60 * 1000 # Refresh token : 7 à 30 jours exp: Date.now() + 7 * 24 * 60 * 60 * 1000
Si JWT dans cookie : → Utiliser SameSite=Strict ou Lax → Vérifier l'origine de la requête → Token CSRF complémentaire pour les mutations
POST /api/auth/register → Créer un compte POST /api/auth/login → Se connecter (obtenir tokens) POST /api/auth/refresh → Renouveler l'access token POST /api/auth/logout → Se déconnecter (révoquer refresh token) GET /api/auth/me → Informations de l'utilisateur connecté
GET /api/users → Liste des utilisateurs (admin) GET /api/users/:id → Détails d'un utilisateur PUT /api/users/:id → Modifier un utilisateur (owner ou admin) DELETE /api/users/:id → Supprimer un utilisateur (admin) GET /api/posts → Liste des posts publics POST /api/posts → Créer un post (authentifié) GET /api/posts/:id → Détails d'un post PUT /api/posts/:id → Modifier un post (owner) DELETE /api/posts/:id → Supprimer un post (owner ou admin)
GET /api/users/me HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwicm9sZXMiOlsidXNlciJdLCJpYXQiOjE3MzU1NTkwMDAsImV4cCI6MTczNTU1OTkwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"username": "john_doe",
"email": "john@example.com",
"profile": {
"firstName": "John",
"lastName": "Doe",
"avatar": "https://cdn.example.com/avatars/123.jpg"
},
"roles": ["user"],
"createdAt": "2025-01-15T10:30:00Z"
}
Site web : jwt.io
Permet de décoder et vérifier un JWT manuellement.
# Login
curl -X POST https://api.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"john_doe","password":"secret123"}'
# Utiliser le token
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -X GET https://api.example.com/api/users/me \
-H "Authorization: Bearer $TOKEN"
# Login http POST https://api.example.com/api/auth/login \ username=john_doe password=secret123 # Utiliser le token http GET https://api.example.com/api/users/me \ "Authorization: Bearer $TOKEN"
Mission: modéliser une stratégie complète de sécurisation JWT pour une API réelle.