Authentification JWT en Laravel : le guide complet
Sanctum couvre la plupart des besoins d'auth SPA et mobile. JWT est le bon outil quand des services indépendants doivent vérifier un token sans store de session partagé. Voici comment l'implémenter correctement en Laravel, de la config aux refresh tokens et à la révocation.
Sanctum gère proprement la majorité des besoins d'auth Laravel : tokens SPA, tokens mobile, tokens API simples. JWT résout un problème plus étroit, vérifier un token sans toucher une base de données ou un store de session partagé, ce qui compte dès que plusieurs services indépendants doivent faire confiance au même token. Si vous n'avez pas ce problème, JWT ajoute de la complexité pour rien. Ce guide couvre quand JWT est le bon choix et comment le construire de bout en bout.
JWT vs Sanctum vs Passport
Un token Sanctum est une ligne en base : chaque requête le vérifie, ce qui rend aussi la révocation instantanée (on supprime la ligne). Un JWT est auto-suffisant, sa signature seule prouve sa validité, donc tout service détenant la clé peut le vérifier hors ligne. Passport est un serveur OAuth2 complet, utile seulement quand des clients tiers demandent un accès au nom de vos utilisateurs.
| Sanctum | JWT | Passport | |
|---|---|---|---|
| Stockage du token | Ligne en base | Rien côté serveur | Base (tables OAuth2) |
| Révocation | Instantanée, on supprime la ligne | Difficile, il faut une blocklist | Instantanée, on révoque le token |
| Vérification hors ligne | Non | Oui, par tout service détenant la clé | Non |
| Serveur OAuth2 | Non | Non | Oui |
| Effort de mise en place | Minimal | Modéré | Élevé |
| Idéal pour | Une app et son propre client SPA ou mobile | Plusieurs services indépendants vérifiant un token | Clients d'API tiers, flux OAuth2 |
Si vous avez une seule app Laravel qui parle à un seul frontend, Sanctum est plus simple et plus sûr. Ne recourez à JWT que lorsque des services indépendants doivent vérifier des tokens sans store de session partagé. Dans le doute, c'est Sanctum qu'il vous faut.
Anatomie d'un JWT
Un JWT a trois parties : un header (algorithme de signature), un payload de claims (sujet, expiration, émetteur, audience, données custom), et une signature. La signature empêche la falsification, mais le payload n'est qu'encodé en base64, pas chiffré. Ne mettez jamais de secrets ou de données sensibles dans les claims, quiconque détient le token peut les décoder et les lire.
Installer et configurer le package
Le tymon/jwt-auth d'origine est silencieux depuis un moment. Le fork communautaire maintenu php-open-source-saver/jwt-auth est un remplaçant direct qui suit les versions actuelles de Laravel et de PHP. Installez-le, publiez la config, générez le secret de signature, pointez le guard api sur le driver jwt, et faites implémenter JWTSubject par le modèle User.
// config/auth.php
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
// app/Models/User.php
class User extends Authenticatable implements JWTSubject
{
public function getJWTIdentifier(): mixed
{
return $this->getKey();
}
public function getJWTCustomClaims(): array
{
return ['role' => $this->role];
}
}Login, me, logout
class AuthController extends Controller
{
public function login(LoginRequest $request)
{
if (! $token = auth('api')->attempt($request->validated())) {
return response()->json(['message' => 'Identifiants invalides'], 401);
}
return $this->tokenResponse($token);
}
public function me()
{
return new UserResource(auth('api')->user());
}
public function logout()
{
auth('api')->logout(); // invalide le token courant
return response()->noContent();
}
private function tokenResponse(string $token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}
}Protéger les routes et lire l'utilisateur
Regroupez les routes protégées sous le middleware auth:api. Dans un contrôleur, le guard api résout le modèle authentifié depuis le token, sans requête en base sauf si vous en demandez une.
// routes/api.php
Route::post('auth/login', [AuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
Route::get('auth/me', [AuthController::class, 'me']);
Route::post('auth/refresh', [AuthController::class, 'refresh']);
Route::post('auth/logout', [AuthController::class, 'logout']);
Route::apiResource('orders', OrderController::class);
});Faire les refresh tokens correctement
Gardez l'access token court (15 à 60 minutes) et associez-le à un refresh token plus long. Faites tourner le refresh token à chaque utilisation, en émettre un nouveau et invalider l'ancien, pour qu'un refresh token volé ne fonctionne qu'une fois avant que le prochain refresh de l'utilisateur légitime ne révèle le vol.
public function refresh()
{
$newToken = auth('api')->refresh(); // l'ancien token est blacklisté, un nouveau est émis
return $this->tokenResponse($newToken);
}Révocation avec une blocklist Redis
Un JWT ne peut pas être dé-émis, donc le logout forcé et le "déconnecter partout" nécessitent une blocklist. Stockez l'identifiant du token (le claim jti) dans Redis avec un TTL égal à la durée de vie restante du token, ainsi l'entrée disparaît d'elle-même une fois que le token aurait de toute façon expiré. Vérifiez la blocklist dans un middleware après auth:api.
class RejectBlockedTokens
{
public function handle(Request $request, Closure $next)
{
$jti = auth('api')->payload()->get('jti');
if (Redis::exists("jwt:blocked:{$jti}")) {
return response()->json(['message' => 'Token révoqué'], 401);
}
return $next($request);
}
}Pièges de sécurité à éviter
- ✓Confusion d'algorithme : fixez l'algorithme attendu côté serveur, ne faites jamais confiance au header alg du token lui-même.
- ✓Ne stockez pas le token dans le localStorage pour une app navigateur, utilisez un cookie httpOnly, Secure, SameSite pour qu'un bug XSS ne puisse pas le voler.
- ✓Validez exp, iss et aud, pas seulement la signature, pour qu'un token émis pour un autre service soit rejeté.
- ✓Gardez le payload petit, il voyage à chaque requête et il est lisible par n'importe qui.
- ✓Servez l'API uniquement en HTTPS, et faites tourner la clé de signature selon un calendrier avec une fenêtre de recouvrement.
Tester l'auth JWT
public function test_une_route_protegee_exige_un_token_valide(): void
{
$user = User::factory()->create();
$token = auth('api')->login($user);
$this->getJson('/api/auth/me')->assertUnauthorized();
$this->withToken($token)
->getJson('/api/auth/me')
->assertOk()
->assertJsonPath('data.id', $user->id);
}FAQ
- JWT ou Sanctum pour une API Laravel ?
- Sanctum pour une seule app Laravel qui parle à votre propre client SPA ou mobile : c'est plus simple, et la révocation est une suppression en base. JWT uniquement quand plusieurs services indépendants doivent vérifier le même token sans partager de store de session ni rappeler votre base. Dans le doute, c'est Sanctum qu'il vous faut.
- Où stocker le token JWT côté client ?
- Dans un cookie httpOnly, Secure, SameSite pour les apps navigateur, pour que JavaScript ne puisse pas le lire et qu'un bug XSS ne puisse pas le voler. Les apps mobiles natives utilisent le stockage sécurisé de la plateforme comme Keychain ou Keystore. Jamais le localStorage.
- Comment révoquer un JWT avant son expiration ?
- On ne peut pas le dé-émettre, donc gardez les access tokens courts (15 à 60 minutes) et maintenez une blocklist des identifiants de token (le claim jti) dans Redis avec un TTL égal à la durée de vie restante du token. Vérifiez la blocklist dans votre middleware d'auth pour le logout forcé et les tokens compromis.
- Quelle durée de vie pour un access token ?
- 15 à 60 minutes pour l'access token, associé à un refresh token de quelques jours à quelques semaines. Des access tokens courts limitent les dégâts d'une fuite ; le refresh token, tourné à chaque utilisation, garde l'utilisateur connecté sans redemander le mot de passe.
- tymon/jwt-auth est-il toujours maintenu ?
- Le package d'origine est silencieux depuis un moment. Le fork communautaire php-open-source-saver/jwt-auth est le remplaçant direct activement maintenu, avec le support des versions actuelles de Laravel et de PHP, et la même API.
JWT résout un problème précis : la vérification stateless entre services indépendants. Implémenté avec des access tokens courts, des refresh tokens tournants, une blocklist Redis et des cookies httpOnly, il est solide. Si ce n'est pas votre architecture, la simplicité de Sanctum et sa révocation instantanée l'emportent à chaque fois.
Besoin d'aide sur ce sujet ? Conception d'API REST
Découvrir ce service →