Authentification JWT
Pour ajouter une authentification JWT (JSON Web Token) au sein de notre API Symfony, on peut utiliser le bundle LexikJWTAuthenticationBundle.
Bien que ce chapitre soit abordé dans la section sur API Platform, l'authentification JWT peut tout à fait être utilisée au sein d'une API qui n'utilise pas API Platform
But
L'authentification JWT est souvent utilisée pour assurer et maintenir une authentification entre un client, type application JS ou mobile, et une API.
Principe
Le principe de fonctionnement est le suivant :
- Côté front-end, s'authentifier puis récupérer la réponse du back-end contenant le JWT
- Trouver un moyen de stocker le JWT côté front-end
- A chaque requête envoyée au back-end ensuite, joindre le JWT dans un header
Authorization - C'est donc le serveur qui s'assure de la validation du JWT
- Le serveur ne stocke pas le JWT, il se contente de le renvoyer à la connexion, puis de le vérifier lors des prochaines requêtes
Différence avec l'authentification par token d'API
Une authentification par token d'API est souvent utilisée pour autoriser l'accès à une application qui souhaiterait accéder à nos données.
Par exemple, le site TMDB permet d'explorer toute une bibliothèque de films sur son propre site web. Mais le site contient également une section API, dans laquelle se trouvent des instructions pour s'inscrire et se créer une clé d'accès au système de TMDB.
Avec cette clé d'API, nous sommes donc capables d'écrire nous-mêmes des applications qui peuvent effectuer des requêtes HTTP et récupérer des données de TMDB au format JSON.
Par la suite, vu que notre token d'API est stocké chez TMDB, ils peuvent alors garder un historique de nos requêtes, ou bien appliquer des restrictions (par minute, par jour, par mois...) au-delà desquelles nous serions obligés de souscrire à une offre payante, par exemple.
L'authentification avec API Token est donc couramment utilisée comme business model pour mettre des systèmes à disposition d'applications tierces.
Un JWT, lui, n'est pas stocké sur le serveur.
Ainsi, en base de données, on stocke vraiment un nom d'utilisateur et un mot de passe. Par ailleurs, le JWT ayant une durée de vie, il permet d'assurer et de maintenir une authentification sur une durée définie.
C'est pour ça que le scénario d'utilisation habituel va être davantage la réalisation d'une application cliente, type React/Vue/Svelte/Angular, qui va gérer toute l'interface et les interactions avec l'utilisateur, tandis que l'API ne représente que l'accès à la couche des données.
Dans une application cliente, on pourra alors présenter un formulaire de login, et lors de la soumission, requêter l'API et lui envoyer les identifiants saisis. S'ils sont corrects, alors l'API renverra un JWT.
| API Token | JWT (JSON Web Token) |
|---|---|
| Pas de durée de vie (stocké en BDD) | Durée de vie définie en configuration, pas stocké côté serveur |
| Pas d'identifiants, seul le token suffit | D'abord des identifiants pour générer le JWT, ensuite, requêtes avec JWT dans les headers |
Installation et configuration
Pour les utilisateurs de Windows, avant l'installation de Lexik, il faut activer l'extension sodium de PHP pour pouvoir avoir la dernière version du bundle :
- Vérifier si l'extension est déjà activée :
php -m
Dans la liste qui s'affiche, chercher un élément sodium.
S'il est présent, alors on peut installer le bundle. Sinon, continuer les instructions dans ce bloc.
- Trouver le chemin vers le fichier de configuration :
php --ini
- Dans le fichier de configuration, chercher
sodium, ce qui devrait amener à une ligne de ce type :
;extension=sodium
-
Enlever le point-virgule
;pour décommenter la ligne et activer l'extension, puis enregistrer le fichier -
Relancer la commande
php -mpour vérifier quesodiumapparaît correctement
La documentation du bundle LexikJWTAuthenticationBundle est assez claire. Il s'agit de l'analyser et comprendre quelles parties nous seront utiles. Par exemple, certaines instructions de configuration concernent les applications Symfony possédant une version inférieure à 5.3. Ces instructions ne nous concernent donc pas.
Installation
composer require lexik/jwt-authentication-bundle
Avec Symfony Flex, le bundle sera automatiquement enregistré dans le fichier config/bundles.php.
Génération des clés SSL
Étant donné qu'un JWT n'est pas stocké côté serveur, alors une fois qu'il est généré, il est envoyé dans la nature. N'importe qui est donc capable d'effectuer des requêtes à notre API en indiquant un JWT dans les en-têtes de requête.
Côté serveur, il est alors important de s'assurer que le JWT est valide, dans le sens où on va vouloir s'assurer que c'est bien ce serveur qui a généré le JWT.
Pour cela, on utilise une paire de clés SSL (une clé publique et une clé privée) afin de signer le JWT.
Nous aurons alors 2 étapes distinctes :
- À la génération d'un JWT (si les identifiants sont corrects), on utilise la clé privée pour signer le JWT
- Lorsqu'on reçoit une requête d'API avec un JWT, on utilise la clé publique pour vérifier que la signature a bien été effectuée par le serveur
Ceci empêche tout acteur extérieur à l'application de générer lui-même un JWT et tenter de se faire passer pour quelqu'un d'autre.
À l'installation, le bundle a enregistré quelques commandes dans la console, notamment la suivante, pour générer la paire de clés automatiquement :
php bin/console lexik:jwt:generate-keypair
Si la commande ne fonctionne pas, il peut être nécessaire d'installer OpenSSL : suivre ce lien
Retenter ensuite d'exécuter la commande
Les clés sont générées automatiquement dans le dossier config/jwt, et ignorées par Git.
Configuration
Dans le fichier de variables d'environnement, il est possible de personnaliser :
- le chemin vers la clé privée
- le chemin vers la clé publique
- La passphrase utilisée pour la génération et la signature du token
Dans le fichier de configuration du package de sécurité (config/packages/security.yaml), nous devons ajouter les pare-feux nécessaires :
security:
# ...
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~
main:
lazy: true
provider: app_user_provider
#...
Attention à l'ordre des pare-feux : ici, les pare-feux login et api se trouvent avant le pare-feu main. Ce dernier est le plus généraliste, celui qui captera toutes les requêtes, en-dehors des requêtes d'API.
Le composant de sécurité ne vérifie pas tous les pare-feux : dès qu'il en trouve un qui correspond à la requête, il s'arrête là et l'exécute.
Pour finir, il ne reste qu'à configurer la route qui saura gérer le login : elle est intégrée au bundle, nous n'avons qu'à la déclarer dans notre application :
api_login_check:
path: /api/login_check
Test de la connexion
Avant de tester la connexion JWT, vérifier les points suivants :
- L'utilisateur avec lequel on tente de se connecter est bien enregistré en base de données
- Pour créer un utilisateur :
- Soit via les fixtures
- Soit directement via une requête
POSTsur le endpoint/api/users
- Pour créer un utilisateur :
- Le mot de passe de l'utilisateur est bien haché en base de données
- Le serveur a bien été relancé avec la commande
symfony serve --no-tls
Depuis un client comme Postman, on pourra forger une requête de connexion sur l'URI /api/login_check :

Vérifier les points suivants :
- Requête
POST - Dans l'onglet "Body", format "raw" coché, et "JSON" sélectionné à droite
Exemple de payload (contenu de la requête) :
{
"username": "emp@bob.com",
"password": "bobby1234"
}
L'API devrait alors renvoyer un JWT dans le corps de la réponse :

Pour aller plus loin
Cookie
Traditionnellement, le JWT sera renvoyé dans le corps de la requête, puis stocké côté client, dans le localStorage par exemple.
Le risque principal de cette approche est qu'un utilisateur se fasse voler son JWT : n'importe quel code Javascript s'exécutant dans le navigateur peut accéder au localStorage et extraire le JWT.
Quiconque parvient à voler un JWT peut alors se faire passer pour l'utilisateur.
Une solution possible, permettant de réduire les possibilités d'attaque, serait de faire transiter le JWT via les cookies. Un avantage particulier de cette méthode est que les cookies peuvent être définis comme httpOnly, donc inaccessible via Javascript.
Il est possible de configurer le bundle Lexik pour qu'il renvoie automatiquement le token dans un cookie, plutôt que dans le corps de la réponse.
Par ailleurs, pour qu'un navigateur accepte bien le cookie renvoyé par le serveur, il sera nécessaire d'ajouter une en-tête de réponse Access-Control-Allow-Credentials à true. Avec un listener sur kernel.response, par exemple :
<?php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
class ResponseListener
{
public function onKernelResponse(ResponseEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
$response = $event->getResponse();
$response->headers->set("Access-Control-Allow-Credentials", "true");
}
}
Refresh Token
Le désavantage principal, en cas de JWT volé, est qu'il est stateless : tant qu'il n'est pas expiré, alors n'importe qui peut l'utiliser pour effectuer des requêtes à notre API, et donc se faire passer pour quelqu'un.
Pour réduire la surface d'attaque (et non la supprimer complètement), on peut également ajouter un système de refresh token.
Le principe est le suivant :
- On configure la durée de vie du JWT à une durée très faible (10 secondes par exemple)
- Côté client, lorsque le JWT a expiré, on régénère automatiquement un JWT à l'aide d'un refresh token
- Et on recommence comme ça, de manière transparente pour l'utilisateur
Si un JWT est volé, alors l'utilisateur malveillant n'a que très peu de temps pour l'utiliser, puisqu'il expirera très rapidement.
Si un refresh token est volé, alors il y a une différence majeure avec les JWT : les refresh tokens sont stockés en base de données. Il est donc possible d'invalider un refresh token pour empêcher la création de nouveaux JWT.
Avec Symfony, cela peut être intégré à l'aide d'un bundle : le JWTRefreshTokenBundle