Skip to Content

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 api avec json_login (renvoie un JWT) + jwt (vérifie le token).
  • Flux : POST /api/login_check avec email/mot de passe → renvoie { "token": "..." }. Les requêtes suivantes envoient ce token en Bearer.
  • 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 login gère POST /api/login_check : json_login vérifie email/mot de passe et, en cas de succès, renvoie un JWT (via le success handler de Lexik).
  • Le firewall api protège ^/api : l’authenticator jwt lit le Bearer token, 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/refresh avec 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 :

StockageSé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 configurer nelmio/cors-bundle cô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

  1. Le front envoie POST /api/login_check avec {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 envoie Authorization: Bearer <JWT>. 5. Le firewall api (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/refresh avec 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.