Chapitre 10.4 — JWT pour l’API (l’auth du front Next.js)
Partie 10 : Sécurité — Chapitre 4/6 Version de référence : Symfony 7.4 / LexikJWTAuthenticationBundle.
⏱️ TL;DR
- L’API est stateless : chaque requête porte un JWT (JSON Web Token) dans l’en-tête
Authorization: Bearer <token>. Pas de session.- On installe LexikJWTAuthenticationBundle, on génère une paire de clés (privée pour signer, publique pour vérifier), et on configure le firewall
apiavecjson_login(renvoie un JWT) +jwt(vérifie le token).- Flux :
POST /api/login_checkavec email/mot de passe → renvoie{ "token": "..." }. Les requêtes suivantes envoient ce token enBearer.- Un access token court (ex. 1 h) + un refresh token (gesdinet) évitent de re-saisir le mot de passe tout en limitant le risque en cas de fuite.
- Côté Next.js : obtenir le token, l’attacher aux
fetch, gérer le 401 et le refresh. Où le stocker (cookie httpOnly vs mémoire) est un choix de sécurité.- Pour un dev Next.js : c’est le pattern JWT auth d’une SPA/mobile que vous connaissez — Symfony fournit l’émission et la vérification, vous gérez le token côté client.
🎯 Objectifs
- Mettre en place l’authentification JWT sur le firewall API.
- Obtenir et utiliser un token.
- Ajouter des refresh tokens.
- Consommer l’auth depuis Next.js en sécurité.
1. Pourquoi JWT pour l’API
Le front Next.js (et un futur mobile) consomme l’API sans session serveur. Un JWT est un jeton auto-suffisant : signé par le serveur, il contient l’identité et les rôles de l’utilisateur, et une date d’expiration. Le serveur le vérifie (via la signature) à chaque requête, sans rien stocker — d’où la scalabilité (aucune affinité de session).
2. Installer et générer les clés
composer require lexik/jwt-authentication-bundle
php bin/console lexik:jwt:generate-keypairÇa crée une clé privée (signe les tokens) et une clé publique (les vérifie) dans config/jwt/, et ajoute la passphrase dans .env. La clé privée est un secret : jamais committée en clair (coffre de secrets, chapitre 3.4).
3. Configurer le firewall API
# config/packages/security.yaml (extrait)
firewalls:
login:
pattern: ^/api/login_check
stateless: true
json_login:
check_path: /api/login_check # l'endpoint de login
username_path: email
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~ # vérifie le Bearer token sur /api/*
access_control:
- { path: ^/api/login_check, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: ROLE_USER }- Le firewall
logingèrePOST /api/login_check:json_loginvérifie email/mot de passe et, en cas de succès, renvoie un JWT (via le success handler de Lexik). - Le firewall
apiprotège^/api: l’authenticatorjwtlit leBearertoken, vérifie la signature et l’expiration, et authentifie l’utilisateur.
4. Obtenir et utiliser un token
Login :
curl -X POST https://localhost:8000/api/login_check \
-H 'Content-Type: application/json' \
-d '{"email":"admin@booklab.test","password":"secret"}'
# → { "token": "eyJhbGciOi...", ... }Requête authentifiée :
curl https://localhost:8000/api/bookings \
-H 'Authorization: Bearer eyJhbGciOi...'Le token contient des claims (identité, rôles, exp d’expiration). On configure sa durée de vie (token_ttl, ex. 3600 s). Court = plus sûr (fenêtre de fuite réduite), mais impose un refresh.
5. Refresh tokens
Un access token court expire vite — on ne veut pas re-saisir le mot de passe toutes les heures. Le refresh token (bundle gesdinet/jwt-refresh-token-bundle) résout ça :
composer require gesdinet/jwt-refresh-token-bundle- Au login, l’API renvoie deux jetons : un access token (court, ex. 1 h) et un refresh token (long, ex. 30 j, stocké en base).
- Quand l’access token expire (401), le client appelle
POST /api/token/refreshavec le refresh token → un nouvel access token, sans mot de passe. - Le refresh token peut être révoqué (déconnexion, compromission) — contrairement à un JWT pur qu’on ne peut pas invalider avant expiration.
⚠️ Piège : un JWT ne peut pas être invalidé avant son expiration (il est auto-suffisant). Si un access token fuit, il reste valide jusqu’à
exp. D’où : access tokens courts + refresh tokens révocables (stockés en base, qu’on peut supprimer). Pour une « déconnexion immédiate » de tous les appareils, on révoque les refresh tokens (et on garde les access tokens courts).
6. Consommer depuis Next.js (et où stocker le token)
Côté front, deux décisions de sécurité :
A) Attacher le token aux requêtes :
// app/lib/api.ts (Next.js) — schéma
async function apiFetch(path: string, token: string, init?: RequestInit) {
const res = await fetch(`${process.env.API_URL}${path}`, {
...init,
headers: { ...init?.headers, Authorization: `Bearer ${token}` },
});
if (res.status === 401) { /* tenter un refresh, sinon rediriger vers login */ }
return res;
}B) Où stocker le token — le point sensible :
| Stockage | Sécurité |
|---|---|
localStorage | ❌ vulnérable au XSS (un script injecté lit le token) |
| Mémoire (state) | 🟡 sûr mais perdu au rechargement |
Cookie httpOnly + Secure + SameSite | ✅ non lisible par JS (anti-XSS), envoyé automatiquement |
La pratique recommandée pour un front Next.js : stocker les tokens dans un cookie httpOnly (posé par une route Next.js côté serveur après login), de sorte que le JS ne les touche jamais. Next.js (Route Handlers / middleware) joue alors le rôle de proxy d’auth entre le navigateur et l’API Symfony. On détaille cette intégration en Partie 11 (avec le CORS).
💡 Pour un dev Next.js : vous connaissez ce débat (localStorage vs httpOnly cookie). Avec l’App Router, le pattern propre est : login via une Server Action / Route Handler qui appelle
/api/login_check, reçoit le JWT, et le pose en cookie httpOnly ; les requêtes côté serveur (RSC) relaient ce cookie vers l’API. Symfony reste l’émetteur/vérificateur ; Next.js gère le transport sécurisé du token. C’est l’architecture qu’on met en place en Partie 11.
⚠️ Piège CORS : dès que le front (
localhost:3000) appelle l’API (localhost:8000), le navigateur applique le CORS. Il faut configurernelmio/cors-bundlecôté Symfony pour autoriser l’origine du front, les en-têtes (Authorization) et les méthodes. On le fait en Partie 11 — retenez que c’est obligatoire dès que les deux tournent sur des origines différentes.
✏️ Exercices
Exercice 1. Décrivez le flux complet, de la saisie du mot de passe côté Next.js à une requête authentifiée sur /api/bookings.
✅ Solution
- Le front envoie
POST /api/login_checkavec{email, password}. 2. Symfony (json_login) vérifie le mot de passe haché et renvoie un JWT (+ refresh token). 3. Le front stocke le token (idéalement cookie httpOnly posé par une route Next.js serveur). 4. Pour chaque requête, le front envoieAuthorization: Bearer <JWT>. 5. Le firewallapi(jwt) vérifie la signature et l’expiration, authentifie l’utilisateur, et le contrôleur/voter décide de l’accès. 6. Si 401 (token expiré), le front appelle/api/token/refreshavec le refresh token pour obtenir un nouvel access token.
Exercice 2. Pourquoi ne peut-on pas « déconnecter » un JWT avant son expiration, et comment contourne-t-on ?
✅ Solution
Un JWT est auto-suffisant : le serveur le valide par sa signature sans le stocker, donc il ne peut pas le « révoquer » — il reste valide jusqu’à exp. Contournement : des access tokens courts (fenêtre de risque réduite) + des refresh tokens révocables (stockés en base, supprimables). Pour déconnecter, on révoque le refresh token ; l’access token, court, expirera vite de lui-même.
Exercice 3. Pourquoi éviter localStorage pour stocker le JWT côté Next.js, et quelle alternative ?
✅ Solution
localStorage est lisible par n’importe quel JavaScript de la page : en cas de faille XSS, l’attaquant lit le token et usurpe la session. Alternative recommandée : un cookie httpOnly + Secure + SameSite, non accessible au JS (donc protégé du XSS) et envoyé automatiquement. Concrètement, une route Next.js serveur pose ce cookie après login, et relaie le token vers l’API.
🧠 Quiz de révision
1. Comment une requête d’API transmet-elle le JWT ?
Dans l’en-tête Authorization: Bearer <token>. L’API est stateless (pas de session).
2. Quel endpoint renvoie le token, et via quelle config ?
POST /api/login_check, via le firewall en json_login (LexikJWT), qui vérifie email/mot de passe et renvoie { token }.
3. À quoi servent les deux clés générées ?
La clé privée signe les tokens (émission), la clé publique les vérifie (validation). La privée est un secret.
4. Pourquoi ajouter des refresh tokens ?
Pour garder des access tokens courts (plus sûrs) sans re-saisir le mot de passe : le refresh token (long, révocable, en base) permet d’obtenir un nouvel access token, et de révoquer l’accès.
5. Meilleur endroit pour stocker le token côté Next.js ?
Un cookie httpOnly + Secure + SameSite (non lisible par JS → anti-XSS), posé par une route Next.js serveur — pas localStorage.
Prochain chapitre : 10.5 — OAuth & connexion sociale — se connecter via Google/GitHub.