Découpler le modèle utilisateur de votre application du système de sécurité de Symfony

Illustration de Découpler le modèle utilisateur de votre application du système de sécurité de Symfony

Séparer l’utilisateur du domaine de l’objet attendu par Symfony Security avec un fournisseur dédié.

Une architecture hexagonale garde les règles métier indépendantes des choix techniques. Pourtant, l’implémentation habituelle de Symfony Security pousse souvent l’entité utilisateur du domaine à implémenter une interface du framework.

Dans une configuration Symfony classique :

les autorisations sont toujours liées à un objet utilisateur. Pour sécuriser tout ou partie de votre application, vous devez créer une classe utilisateur. Cette classe implémente UserInterface. Il s’agit souvent d’une entité Doctrine [4].

Cette approche est simple, mais elle couple le domaine à Symfony.

Une autre approche consiste à créer une classe réservée à la sécurité [1, 2, 3].

Les articles cités en référence précèdent plusieurs évolutions de Symfony Security. Voici la même séparation avec les API actuelles.

Séparer les deux modèles

  1. Créer une classe utilisateur distincte pour la sécurité, SecurityUser.php.

  2. Créer son fournisseur, SecurityUserProvider.php.

  3. Configurer les deux services dans config/packages/security.yaml.

SecurityUser. Cette classe représente l’utilisateur attendu par Symfony sans modifier l’entité User du domaine. Elle implémente UserInterface et les autres interfaces requises par le système de sécurité.

PHP
<?php

declare(strict_types=1);

namespace Infrastructure\Framework\Symfony\Security;

use Domain\Model\User\Entity\User;
use Symfony\Component\Security\Core\User\{
    UserInterface,
    PasswordAuthenticatedUserInterface
};
use Symfony\Component\Uid\Uuid;

final readonly class SecurityUser implements
    UserInterface,
    PasswordAuthenticatedUserInterface
{
    private function __construct(
        private Uuid $id,
        private string $email,
        private string $password,
        private array $roles
    ) {
    }

    public static function create(User $user): self
    {
        return new self(
            $user->getId(),
            $user->getEmail(),
            $user->getPassword(),
            $user->getRoles()
        );
    }

    #[\Override]
    public function getPassword(): ?string
    {
        return $this->password;
    }

    #[\Override]
    public function getRoles(): array
    {
        return $this->roles;
    }

    #[\Override]
    public function getUserIdentifier(): string
    {
        return $this->email;
    }

     #[\Override]
    public function eraseCredentials(): void
    {
    }
}

SecurityUserProvider. Le fournisseur d’utilisateurs récupère SecurityUser à partir d’un identifiant, ici une adresse e-mail. Il charge d’abord l’entité User depuis le dépôt du domaine, puis l’adapte au type attendu par Symfony.

PHP
<?php

declare(strict_types=1);

namespace Infrastructure\Framework\Symfony\Security;

use Domain\Model\User\Repository\UserRepository;
use Symfony\Component\Security\Core\{
    Exception\UserNotFoundException,
    User\UserInterface,
    User\UserProviderInterface
};

final readonly class SecurityUserProvider implements UserProviderInterface
{
    public function __construct(
        private UserRepository $userRepository
    ) {
    }

    #[\Override]
    public function refreshUser(UserInterface $user): UserInterface
    {
        return $this->loadUserByIdentifier($user->getUserIdentifier());
    }

    #[\Override]
    public function loadUserByIdentifier(string $identifier): UserInterface
    {
        $user = $this->userRepository->getByEmail($email);
        if ($user === null) {
            throw new UserNotFoundException();
        }

        return SecurityUser::create($user);
    }

    #[\Override]
    public function supportsClass(string $class): bool
    {
        return $class === SecurityUser::class;
    }
}

Il ne reste plus qu’à déclarer le fournisseur d’utilisateurs auprès de Symfony dans config/packages/security.yaml :

YAML
security:
    # ...
    providers:
        app_user_provider:
            id: Infrastructure\Framework\Symfony\Security\SecurityUserProvider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout
                target: app_login

    access_control:
        - { path: ^/login$, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: ROLE_USER }

Accéder aux données du domaine après la connexion

Symfony propose plusieurs authentificateurs intégrés, comme form_login, qui simplifient l’implémentation de l’authentification des utilisateurs. Si votre application exige toutefois une logique d’authentification plus complexe, vous pouvez créer un authentificateur personnalisé et vous appuyer sur le SecurityUserProvider que j’ai implémenté pour charger les utilisateurs pendant le processus d’authentification.

PHP
public function authenticate(Request $request): Passport
{
    $token = $request->request->get('_csrf_token');
	$email = $request->request->get('email');
	$password = $request->request->get('password');

	$request
		->getSession()
		->set(SecurityRequestAttributes::LAST_USERNAME, $email);

	$passport = new Passport(
		 new UserBadge($email, $this->securityUserProvider->loadUserByIdentifier(...)),
		 new PasswordCredentials($password),
		 [
		    new CsrfTokenBadge('authenticate', $token),
		    new RememberMeBadge(),
		 ]
	);

	return $passport;
}

Lorsque vous récupérez l’utilisateur connecté avec getUser() ou app.user dans Twig, Symfony renvoie SecurityUser, pas le User du domaine. SecurityUser sert uniquement à l’authentification et à l’autorisation.

Si une vue a besoin du profil ou d’autres données métier, chargez un modèle de lecture dédié depuis l’identifiant de SecurityUser. Le contrôleur obtient les données nécessaires sans exposer l’entité du domaine à Symfony Security.

Références

  1. https://simshaun.medium.com/decoupling-your-application-user-from-symfonys-security-user-60fa31b4f7f2

  2. https://matthiasnoback.nl/2022/07/decoupling-your-security-user-from-your-user-model/

  3. https://stovepipe.systems/post/decoupling-your-security-user

  4. https://symfony.com/doc/7.2/security.html

Articles liés