« JWT SPA symfony exercice pratique » : différence entre les versions
Aucun résumé des modifications |
|||
| (23 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 1 : | Ligne 1 : | ||
Source: ? (perdue, disparue) | |||
== Introduction == | |||
Chanter JWT pour l'authentification dans un SPA avec Symfony est très facile grâce à des bundles comme FOSUserBundle, LexikJWTAuthenticationBundle et JWTRefreshTokenBundle. Malheureusement, ils ne fournissent pas de sécurité contre les attaques XSS (ce qui, selon vous, n'est pas vraiment là). Dans cet article, je veux montrer comment nous pouvons améliorer une application Symfony en utilisant ces bundles pour empêcher les attaques XSS. Comme il est assez long, je vais le diviser en parties suivantes: | Chanter JWT pour l'authentification dans un SPA avec Symfony est très facile grâce à des bundles comme FOSUserBundle, LexikJWTAuthenticationBundle et JWTRefreshTokenBundle. Malheureusement, ils ne fournissent pas de sécurité contre les attaques XSS (ce qui, selon vous, n'est pas vraiment là). Dans cet article, je veux montrer comment nous pouvons améliorer une application Symfony en utilisant ces bundles pour empêcher les attaques XSS. Comme il est assez long, je vais le diviser en parties suivantes: | ||
| Ligne 59 : | Ligne 63 : | ||
Depuis la version 0.7.0, le bundle de jetons d'actualisation contient une nouvelle option single_use qui supprime un jeton d'actualisation après utilisation et en émet un nouveau. Cela signifie qu'une fois qu'un attaquant vole un jeton d'actualisation, il n'a que jusqu'à la prochaine utilisation de l'utilisateur pour utiliser le jeton d'actualisation. Après c'est déjà inutile. Alors activons cela dans le fichier gesdinet_jwt_refresh_token.yml. | Depuis la version 0.7.0, le bundle de jetons d'actualisation contient une nouvelle option single_use qui supprime un jeton d'actualisation après utilisation et en émet un nouveau. Cela signifie qu'une fois qu'un attaquant vole un jeton d'actualisation, il n'a que jusqu'à la prochaine utilisation de l'utilisateur pour utiliser le jeton d'actualisation. Après c'est déjà inutile. Alors activons cela dans le fichier gesdinet_jwt_refresh_token.yml. | ||
gesdinet_jwt_refresh_token.yaml | ==== gesdinet_jwt_refresh_token.yaml ==== | ||
< | <syntaxhighlight lang="yaml" line> | ||
gesdinet_jwt_refresh_token: | |||
single_use: true | |||
</ | </syntaxhighlight> | ||
Cela rend évidemment le paramètre ttl_update: true inutile. | Cela rend évidemment le paramètre ttl_update: true inutile. | ||
| Ligne 72 : | Ligne 76 : | ||
Pour cela, nous ajoutons un gestionnaire de déconnexion personnalisé à notre pare-feu qui est généralement appelé api dans mes applications. Cela signifie que j'utilise la fonctionnalité de déconnexion par défaut de Symfony, mais en étendant la gestion. | Pour cela, nous ajoutons un gestionnaire de déconnexion personnalisé à notre pare-feu qui est généralement appelé api dans mes applications. Cela signifie que j'utilise la fonctionnalité de déconnexion par défaut de Symfony, mais en étendant la gestion. | ||
security.yaml | ==== security.yaml ==== | ||
< | <syntaxhighlight lang="yaml" line> | ||
security: | security: | ||
firewalls: | firewalls: | ||
| Ligne 80 : | Ligne 84 : | ||
logout: | logout: | ||
path: /api/logout | path: /api/logout | ||
handlers: | handlers: App\Service\Authentication\LogoutHandler | ||
</ | </syntaxhighlight> | ||
Dans le gestionnaire, nous injectons simplement la connexion à la base de données et supprimons les jetons. Le référentiel du bundle ne prend pas en charge la suppression de tous les jetons d'actualisation pour un utilisateur, mais vous pouvez également étendre l'implémentation RefreshTokenManager, ajouter une méthode de référentiel et l'injecter ici à la place. | Dans le gestionnaire, nous injectons simplement la connexion à la base de données et supprimons les jetons. Le référentiel du bundle ne prend pas en charge la suppression de tous les jetons d'actualisation pour un utilisateur, mais vous pouvez également étendre l'implémentation RefreshTokenManager, ajouter une méthode de référentiel et l'injecter ici à la place. | ||
LogoutHandler | ==== LogoutHandler ==== | ||
< | <syntaxhighlight lang="php" line> | ||
<?php | <?php | ||
declare(strict_types=1); | declare(strict_types=1); | ||
| Ligne 121 : | Ligne 125 : | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
=== Ajouter un cookie à l'authentification JWT === | === Ajouter un cookie à l'authentification JWT === | ||
| Ligne 129 : | Ligne 133 : | ||
Et la meilleure chose: nous pouvons créer le cookie au-dessus du jeton d'actualisation de l'utilisateur. Cela signifie que chaque fois que le jeton d'actualisation est supprimé, le cookie est également invalidé. | Et la meilleure chose: nous pouvons créer le cookie au-dessus du jeton d'actualisation de l'utilisateur. Cela signifie que chaque fois que le jeton d'actualisation est supprimé, le cookie est également invalidé. | ||
==== Configuration des cookies ==== | |||
La valeur doit être basée sur l'utilisateur, le jeton d'actualisation valide actuel et un sel aléatoire. De cette façon, il ne dépend toujours pas de la session du serveur mais des valeurs de la base de données. Nous aurions également la possibilité de forcer une déconnexion globale en modifiant le sel utilisé sur le serveur. Une petite méthode pour cela pourrait ressembler à ceci: | La valeur doit être basée sur l'utilisateur, le jeton d'actualisation valide actuel et un sel aléatoire. De cette façon, il ne dépend toujours pas de la session du serveur mais des valeurs de la base de données. Nous aurions également la possibilité de forcer une déconnexion globale en modifiant le sel utilisé sur le serveur. Une petite méthode pour cela pourrait ressembler à ceci: | ||
generateSecurityCookieHash | ===== generateSecurityCookieHash ===== | ||
< | <syntaxhighlight lang="php" line> | ||
private function generateSecurityCookieHash(User $user, RefreshTokenInterface $refreshToken): string | private function generateSecurityCookieHash(User $user, RefreshTokenInterface $refreshToken): string | ||
{ | { | ||
| Ligne 141 : | Ligne 145 : | ||
); | ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
Comme nous allons avoir besoin de cela et de quelques méthodes supplémentaires, nous allons créer un service central appelé AuthenticationService. | Comme nous allons avoir besoin de cela et de quelques méthodes supplémentaires, nous allons créer un service central appelé AuthenticationService. | ||
| Ligne 147 : | Ligne 151 : | ||
Dans celui-ci, nous créerons le cookie en fonction du jeton d'actualisation de l'utilisateur authentifié. Le service complet ressemblera à ceci: | Dans celui-ci, nous créerons le cookie en fonction du jeton d'actualisation de l'utilisateur authentifié. Le service complet ressemblera à ceci: | ||
AuthenticationService | ===== AuthenticationService ===== | ||
< | <syntaxhighlight lang="php" line> | ||
<?php | <?php | ||
declare(strict_types=1); | declare(strict_types=1); | ||
| Ligne 221 : | Ligne 225 : | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
Nous utilisons la même date d'expiration pour le cookie que pour le jeton d'actualisation et le marquons comme http uniquement. | Nous utilisons la même date d'expiration pour le cookie que pour le jeton d'actualisation et le marquons comme http uniquement. | ||
Définir un cookie lors de l'authentification | ==== Définir un cookie lors de l'authentification ==== | ||
Pour définir le cookie, nous ajoutons un gestionnaire de réussite de connexion et utilisons notre service d'authentification là-bas: | Pour définir le cookie, nous ajoutons un gestionnaire de réussite de connexion et utilisons notre service d'authentification là-bas: | ||
security.yaml | ===== security.yaml ===== | ||
< | <syntaxhighlight lang="yaml" line> | ||
security: | security: | ||
firewalls: | firewalls: | ||
| Ligne 238 : | Ligne 242 : | ||
check_path: /api/login | check_path: /api/login | ||
success_handler: App\Service\Authentication\LoginSuccessHandler | success_handler: App\Service\Authentication\LoginSuccessHandler | ||
</ | </syntaxhighlight> | ||
LoginSuccessHandler | ===== LoginSuccessHandler ===== | ||
< | <syntaxhighlight lang="php" line> | ||
<?php | |||
declare(strict_types=1); | declare(strict_types=1); | ||
namespace App\Service\Authentication; | namespace App\Service\Authentication; | ||
| Ligne 288 : | Ligne 292 : | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
Nous allons maintenant obtenir un cookie de sécurité que nous vous enverrons à chaque demande et qui n'est pas accessible par Javascript. Mais ce n'est pas évalué pour le moment, alors faisons-le. | Nous allons maintenant obtenir un cookie de sécurité que nous vous enverrons à chaque demande et qui n'est pas accessible par Javascript. Mais ce n'est pas évalué pour le moment, alors faisons-le. | ||
==== Validez le cookie à chaque demande ==== | |||
Vient maintenant la partie qui ne me satisfait toujours pas, car je n'ai pas trouvé de bon point d'entrée. Le garde utilisé lors de l'utilisation du bundle JWT est le JWTTokenAuthenticator. Le moyen le plus simple d'attacher le cookie serait d'écouter la réponse, mais malheureusement, il n'y a aucun moyen d'y parvenir. Et dans tous les cas où j'ai accès à l'utilisateur, nous n'avons pas accès à la réponse. Donc, le seul moyen que j'ai trouvé était de créer mon propre authentificateur, d'étendre le JWTTokenAuthenticator, d'utiliser la même méthode onAuthenticationSuccess et d'appeler la méthode parente pour avoir le même comportement qu'avant juste avec la vérification des cookies ajoutée. L'authentificateur complet sera configuré comme authentificateur dans le fichier security.yml et ressemble à ceci. | Vient maintenant la partie qui ne me satisfait toujours pas, car je n'ai pas trouvé de bon point d'entrée. Le garde utilisé lors de l'utilisation du bundle JWT est le JWTTokenAuthenticator. Le moyen le plus simple d'attacher le cookie serait d'écouter la réponse, mais malheureusement, il n'y a aucun moyen d'y parvenir. Et dans tous les cas où j'ai accès à l'utilisateur, nous n'avons pas accès à la réponse. Donc, le seul moyen que j'ai trouvé était de créer mon propre authentificateur, d'étendre le JWTTokenAuthenticator, d'utiliser la même méthode onAuthenticationSuccess et d'appeler la méthode parente pour avoir le même comportement qu'avant juste avec la vérification des cookies ajoutée. L'authentificateur complet sera configuré comme authentificateur dans le fichier security.yml et ressemble à ceci. | ||
security.yaml | ===== security.yaml ===== | ||
< | <syntaxhighlight lang="yaml" line> | ||
security: | security: | ||
firewalls: | firewalls: | ||
| Ligne 307 : | Ligne 311 : | ||
authenticators: | authenticators: | ||
- App\Service\Authentication\JWTAndSecurityCookieAuthenticator | - App\Service\Authentication\JWTAndSecurityCookieAuthenticator | ||
</ | </syntaxhighlight> | ||
JWTAndSecurityCookieAuthenticator | ===== JWTAndSecurityCookieAuthenticator ===== | ||
< | <syntaxhighlight lang="php" line> | ||
<?php | |||
declare(strict_types=1); | declare(strict_types=1); | ||
namespace App\Service\Authentication; | namespace App\Service\Authentication; | ||
| Ligne 359 : | Ligne 363 : | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
| Ligne 366 : | Ligne 370 : | ||
Faites-moi savoir si vous connaissez une meilleure option pour intégrer cette fonctionnalité. J'écrirai les responsables du bundle JWT et voir si nous pouvons trouver un écouteur d'événement que je pourrais utiliser ici. | Faites-moi savoir si vous connaissez une meilleure option pour intégrer cette fonctionnalité. J'écrirai les responsables du bundle JWT et voir si nous pouvons trouver un écouteur d'événement que je pourrais utiliser ici. | ||
Valider la demande de jeton d'actualisation | === Valider la demande de jeton d'actualisation === | ||
La demande d'actualisation d'un jeton n'est pas gérée par le pare-feu car elle ne peut pas être authentifiée car vous ne demandez un nouveau jeton que si l'ancien a expiré. Il faut donc que cela fonctionne sans authentification. Mais nous pouvons toujours valider le cookie ici aussi. Bien que la façon de le faire soit encore pire que l'authentificateur. | La demande d'actualisation d'un jeton n'est pas gérée par le pare-feu car elle ne peut pas être authentifiée car vous ne demandez un nouveau jeton que si l'ancien a expiré. Il faut donc que cela fonctionne sans authentification. Mais nous pouvons toujours valider le cookie ici aussi. Bien que la façon de le faire soit encore pire que l'authentificateur. | ||
Nous avons un problème similaire à celui du bundle JWT en ce sens qu'il y a des événements que nous pourrions écouter, mais jamais ceux avec un accès à la demande et à la réponse en même temps. Par conséquent, la seule solution que j'ai trouvée était de copier le service de jeton d'actualisation utilisé comme point de terminaison pour l'itinéraire de jeton d'actualisation et de l'étendre. Signification au lieu de cette route qui est la valeur par défaut: | Nous avons un problème similaire à celui du bundle JWT en ce sens qu'il y a des événements que nous pourrions écouter, mais jamais ceux avec un accès à la demande et à la réponse en même temps. Par conséquent, la seule solution que j'ai trouvée était de copier le service de jeton d'actualisation utilisé comme point de terminaison pour l'itinéraire de jeton d'actualisation et de l'étendre. Signification au lieu de cette route qui est la valeur par défaut: | ||
routes.yaml | ===== routes.yaml ===== | ||
< | <syntaxhighlight lang="yaml" line> | ||
api_refresh_token: | api_refresh_token: | ||
path: '/api/token/refresh' | path: '/api/token/refresh' | ||
defaults: { _controller: gesdinet.jwtrefreshtoken:refresh } | defaults: { _controller: gesdinet.jwtrefreshtoken:refresh } | ||
methods: [POST] | methods: [POST] | ||
</ | </syntaxhighlight> | ||
*Nous utilisons le suivant | *Nous utilisons le suivant | ||
<syntaxhighlight lang="yaml" line> | |||
api_refresh_token: | api_refresh_token: | ||
path: '/api/token/refresh' | path: '/api/token/refresh' | ||
defaults: { _controller: App\Service\Authentication\RefreshTokenSecurityCookieService:refresh } | defaults: { _controller: App\Service\Authentication\RefreshTokenSecurityCookieService:refresh } | ||
methods: [POST] | methods: [POST] | ||
</ | </syntaxhighlight> | ||
La copie complète est annotée avec les espaces où je place la validation du cookie et la création du cookie après l'ajout du nouveau jeton d'actualisation. | La copie complète est annotée avec les espaces où je place la validation du cookie et la création du cookie après l'ajout du nouveau jeton d'actualisation. | ||
RefreshTokenSecurityCookieService | ===== RefreshTokenSecurityCookieService ===== | ||
< | <syntaxhighlight lang="php" line> | ||
<?php | |||
declare(strict_types=1); | declare(strict_types=1); | ||
namespace App\Service\Authentication; | namespace App\Service\Authentication; | ||
| Ligne 533 : | Ligne 536 : | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
De cette façon, lorsqu'un nouveau JWT est demandé, le jeton d'actualisation et le cookie sont validés. Un nouveau cookie est également émis ici, car chaque fois que le JWT est demandé, un nouveau jeton d'actualisation est émis et l'ancien cookie a donc été invalidé. | De cette façon, lorsqu'un nouveau JWT est demandé, le jeton d'actualisation et le cookie sont validés. Un nouveau cookie est également émis ici, car chaque fois que le JWT est demandé, un nouveau jeton d'actualisation est émis et l'ancien cookie a donc été invalidé. | ||
[[Category:Symfony]] [[Category:JWT]] | |||