Cet article a été mis à jour le 01/11/2024 à 13:10

Dans cet article, nous allons voir comment mettre en place un système d’authentification JWT sécurisé en divisant le token en deux cookies distincts. En utilisant Symfony 7, LexikJWTAuthenticationBundle et API Platform, cette méthode permet de renforcer la sécurité de votre application en limitant les risques d’interception de token.

🎯 Objectif : Protéger votre application efficacement

Problème : Un seul token peut être risqué

Utiliser un token JWT à longue durée de vie présente des risques : un attaquant qui l’intercepte pourrait se connecter à votre application pour une période indéfinie, et ce, sans avoir besoin du mot de passe de l’utilisateur. Pour éviter cela, diviser le token JWT en deux cookies permet de réduire les risques et de protéger les informations sensibles.

Solution : Diviser le token JWT en deux cookies

Pour maximiser la sécurité, la technique du split token divise le JWT en deux parties et utilise des cookies sécurisés pour stocker ces morceaux de token. Cela évite les inconvénients d’un refresh token, souvent plus exposé dans ce type d’architecture.

🤔 Pourquoi ne pas utiliser un refresh token ?

Qu’est-ce qu’un refresh token ?

Le refresh token est un moyen de renouveler un JWT token sans demander à l’utilisateur de ressaisir son mot de passe. Il permet ainsi de prolonger la session utilisateur.

Les limites de sécurité du refresh token

Le refresh token présente un risque : s’il est intercepté, un attaquant peut l’utiliser pour générer des JWT tokens sans limite et maintenir la session active sans interruption. La technique du split token permet d’éviter ce risque en se passant de refresh token.

💡 Solution : Diviser le token JWT en deux cookies

Fonctionnement : Le split token

La technique consiste à diviser le JWT en deux parties : un cookie contenant le header et le payload, et un autre cookie pour la signature. Le cookie avec la signature est en mode HTTPOnly, ce qui empêche son accès via JavaScript et limite les risques d’interception. Le cookie avec le header et le payload est, lui, accessible côté client pour certaines opérations.

Schéma du fonctionnement du split token Crédit : Peter Locke, dans son article Getting Token Authentication Right in a Stateless Single Page Application

Pour une analyse plus détaillée, cet article offre un complément utile, notamment dans la section "Keeping A User Authenticated with the Right UX: The Two Cookie JWT Approach".

🛠️ Mise en place dans Symfony 7

Voici comment configurer le split token dans un projet Symfony 7 avec API Platform pour un niveau de sécurité élevé.

🔧 Étape 1 : Configuration de LexikJWTAuthenticationBundle

  1. Désactivez le bearer token et activez le split cookie.
  2. Configurez les cookies jwt_hp (header/payload) et jwt_s (signature) dans lexik_jwt_authentication.yaml :
lexik_jwt_authentication:
    token_extractors:
        authorization_header:
            enabled: false
        split_cookie:
            enabled: true
            cookies:
                - jwt_hp
                - jwt_s
    set_cookies:
        jwt_hp:
            lifetime: null
            samesite: strict
            path: /
            domain: null
            httpOnly: false
            secure: true
            split:
                - header
                - payload

        jwt_s:
            lifetime: 0
            samesite: strict
            path: /
            domain: null
            httpOnly: true
            secure: true
            split:
                - signature

🛡️ Étape 2 : Configuration des firewalls

Dans la configuration des firewalls, on définit un point d’entrée JWT et on configure json_login pour que le success_handler et le failure_handler soient gérés par LexikJWTAuthenticationBundle :

security:
    firewalls:
        main:
            stateless: false
            provider: app_admin_provider
            entry_point: jwt
            json_login:
                check_path: /api/auth
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            jwt: ~
            logout:
                path: /api/auth/logout

🔄 Étape 3 : Créer les routes d'authentification

Dans cette configuration, la route /api/auth est utilisée pour l’authentification et /api/auth/logout pour la déconnexion. Voici le code du contrôleur :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class CheckAuth extends AbstractController
{
    #[Route('/api/auth/check', name: 'api_check_auth', methods: ['GET'])]
    public function checkAuthAction(): JsonResponse
    {
        if(is_null($this->getUser())) {
            return new JsonResponse(['error' => 'Unauthorized'], 401);
        }else {
            return new JsonResponse(['success' => 'Authorized'], 200);
        }
    }

    #[Route('/api/auth/logout', name: 'api_logout')]
    public function logout(): JsonResponse
    {
        throw new \LogicException('Cette méthode est interceptée par la configuration de sécurité.');
    }
}

🚪 Étape 4 : Listener pour la déconnexion

Pour garantir que les cookies soient supprimés lors de la déconnexion, on ajoute un listener LogoutListener car par défaut LexikJWTAuthenticationBundle ne supprime pas les cookies dans la configuration split token :

<?php

namespace App\Listener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Event\LogoutEvent;

#[AsEventListener(event: LogoutEvent::class, method: 'onLogout')]
class LogoutListener
{
    public function onLogout(LogoutEvent $logoutEvent): void
    {
        setcookie('jwt_hp', '', -1, '/');
        setcookie('jwt_s', '', -1, '/');
        $logoutEvent->setResponse(new JsonResponse([]));
    }
}

📑 Étape 5 : Configurer API Platform pour l'authentification

Pour que API Platform reconnaisse l'authentification JWT, on ajoute la configuration pour que JWT soit envoyé dans les en-têtes :

api_platform:
  swagger:
    api_keys:
      JWT:
        name: Authorization
        type: header

Enfin, on complète la configuration dans LexikJWTAuthenticationBundle pour que le Swagger d'API Platform prenne en compte la route d'authentification :

lexik_jwt_authentication:
    api_platform:
        check_path: /api/auth
        username_path: email
        password_path: password

Pour encore plus de détails, vous pouvez consulter la documentation officielle d’API Platform sur la configuration JWT. Notez cependant que la documentation n’aborde pas la configuration spécifique au split token.

🎉 Conclusion

Vous avez maintenant une authentification JWT sécurisée en utilisant deux cookies pour diviser le token. Cette méthode renforce la sécurité en réduisant les risques d’interception tout en s’intégrant harmonieusement avec Symfony 7 et API Platform. Il ne vous reste plus qu’à connecter votre front-end pour profiter de ce nouveau système d'authentification sécurisé.

N'attendez plus pour lancer votre projet

Prenons contact