Accueil /  Blog / Symfony / Bloquer la connexion simultanée à un même compte ? C'est possible !

Bloquer la connexion simultanée à un même compte ? C'est possible !

Publié le jeudi 8 septembre 2022

Symfony nous fournit ce qu'il faut pour gérer un système d'authentification de base. Mais parfois il faut pouvoir aller au-delà. Ce sera le cas si vous souhaitez limiter la connexion à un utilisateur en simultanée.

Bien sûr, vous n'aurez pas souvent ce genre de problématique. Mais si votre application a le besoin de métier de s'assurer qu'un seul utilisateur soit connecté à la fois, on vous demandera d'implémenter un tel système. C'est le cas si par exemple l'utilisateur de votre plateforme a un abonnement et qu'il faut vérifier la légitimité de sa connexion.

Le système de connexion par défaut

A priori, tout développeur a déjà pu avoir à mettre en oeuvre l'implémentation standard de la gestion d'utilisateurs de Symfony. On se retrouve le plus souvent avec un authenticator à base de quelques fonctions clefs ou, si vous êtes sur les versions les plus récentes, le nouveau système de badges.

Mais comment bloquer la connexion simultanée à un même compte ? La théorie est plutôt simple, stocker la session de l'utilisateur dans un système de base de données afin d'avoir connaissance de l'existence d'une session pour pouvoir la remplacer par une nouvelle si nécessaire.

Mise en oeuvre

Pour cet exemple, nous avons choisi de travailler avec Redis pour stocker nos sessions. Il nous faudra donc dans un premier temps surcharger le système déjà en place afin de modifier le comportement du SessionHandler, Puis dans un second temps adapter la configuration pour appliquer notre nouvel handler.

Surcharger RedisSessionHandler

Le but de cette surcharge est de venir rajouter un peu de contexte sur la fonction doWrite pour cela on aura besoin du gestionnaire de token ainsi que de l'entity manager pour mettre à jour notre utilisateur. Il nous faudra gérer ensuite 2 cas :

  • L'utilisateur ne possède pas de session, dans ce cas-là on renseigne tout simplement l'id relatif à la session stockée dans Redis.
  • L'utilisateur a déjà une session, dans ce cas-là on détruit la session redis puis on renseigne le nouvel id de session de notre utilisateur.
<?php

namespace App\Session\Storage\Handler;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Redis;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler as NativeRedisSessionHandler
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class RedisSessionHandler extends NativeRedisSessionHandler
{

    public function __construct(
        private \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis,
        private EntityManagerInterface $entityManager,
        private TokenStorageInterface $tokenStorage,
        private array $options = [])
    {
        $this->tokenStorage = $tokenStorage;
        $this->entityManager = $entityManager;
        $options['ttl'] = 43200;
        parent::__construct($redis, $options);
    }

    protected function doWrite($sessionId, $data): bool
    {
        $user = $this->tokenStorage->getToken()->getUser();

        if ($user instanceof User) {
            if($user->getSessionId() === null) {
                $user->setSessionId($sessionId);
                $this->entityManager->flush();
            }

            if($user->getSessionId() !== $sessionId) {
                $this->doDestroy($user->getSessionId());
                $user->setSessionId($sessionId);
                $this->entityManager->flush();
            }
        }

        return parent::doWrite($sessionId, $data);
    }
}

Configuration

Deux configurations sont nécessaires au bon fonctionnement de notre système.

La première se trouve dans framework.yaml et a pour but de préciser le Handler à utiliser et donc pour notre cas, c'est ici qu'il faut renseigner l'utilisation de notre classe.

framework:
     session:
          handler_id: App\Session\Storage\Handler\RedisSessionHandler

La deuxième configuration, elle, permet le bon fonctionnement de notre SessionHandler.

En effet, afin de fonctionner il faut bien lui renseigner l'instance de notre client Redis. De ce fait, direction le services.yaml. On aura donc :

Redis:
     class: Redis
     calls:
          - connect:
               - "%env(REDIS_HOST)%"
               - "%env(int:REDIS_PORT)%"

App\Session\Storage\Handler\RedisSessionHandler:
     arguments:
          - '@Redis'

Pour aller plus loin

Bien sûr, dans notre exemple assez basique, nous faisons le choix de supprimer purement et simplement la session précédente de l'utilisateur. Mais nous pouvons imaginer avoir d'autres actions comme afficher un message d'avertissement et de confirmation à l'utilisateur, ou envoyer une notification à un administrateur l'avertissant de connexions simultanées. Sans compter le fait que vous n'êtes pas obligé d'utiliser Redis, un autre stockage en base sera envisageable pour pouvoir les manipuler. L'essentiel est de pouvoir surcharger l'implémentation standard de Symfony pour ajouter la fonctionnalité que vous voulez.

Suivez notre actualité en avant première. Pas plus d’une newsletter par mois.