12/03/2023

1. Authentification

Lu 607 fois Licence Creative Commons

Connexion

Il est temps de passer à la page de connexion. C'est un système complexe mais bien géré par Symfony. De plus, la génération quasi-complète de la connexion peut se faire à l'aide de la commande make:auth !

Lancer la commande et sélectionner un authentificateur par formulaire de connexion:

php bin/console make:auth
[output]
[output] What style of authentication do you want? [Empty authenticator]:
[output]  [0] Empty authenticator
[output]  [1] Login form authenticator
 > 1

Choisir un nom pour l'authentificateur ainsi que le controlleur:

[output] The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > LoginFormAuthenticator
[output]
[output] Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 > 

Pour terminer, accepter la création d'une route de déconnexion:

[output] Do you want to generate a '/logout' URL? (yes/no) [yes]:
 > 

Controlleur

Le controlleur généré App\Controller\SecurityController comporte les routes de connexion et déconnexion. Pour la déconnexion, il n'y a pas de template, le controlleur ne sera pas exécuté car la route sera interceptée selon la configuration.
Pour la connexion, un service AuthenticationUtils permet de récupérer l'identifiant de l'utilisateur et un message d'erreur, en cas d'échec d'authentification.

Template

Le template /templates/security/login.html.twig généré donne un aperçu avec Bootstrap. Remplacer le contenu pour utiliser les classes CSS de Bulma:

{% extends '_template.html.twig' %}

{% block title 'Connexion' %}

{% block content %}
    <div class="columns is-centered section">
        <div class="column is-two-fifths box p-6">
            <h1 class="title has-text-primary has-text-centered">Connexion</h1>

            {# si l'utilisateur est déjà connecté #}
            {% if app.user %}
                <div class="notification is-info">
                    Vous êtes connecté en tant que {{ app.user.userIdentifier }}, <a href="{{ path('app_logout') }}">Déconnexion</a>
                </div>
            {% endif %}

            {# si une erreur survient #}
            {% if error %}
                <div class="notification is-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
            {% endif %}

            {# formulaire de connexion #}
            <form method="post">
                <div class="field">
                    <label class="label" for="inputUsername">Email ou pseudo</label>
                    <div class="control">
                        <input class="input" type="text" value="{{ last_username }}" name="username" id="inputUsername" required autofocus>
                    </div>
                </div>

                <div class="field">
                    <label class="label" for="inputPassword">Mot de passe</label>
                    <div class="control">
                        <input class="input" type="password" name="password" id="inputPassword" required>
                    </div>
                </div>

                {# un champ caché pour la protection des failles CSRF #}
                <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
                <button class="button is-primary is-fullwidth" type="submit">
                    Connexion
                </button>
            </form>
        </div>
    </div>
{% endblock %}

Ajouter le lien dans le menu dans /templates/_template.html.twig:

{# ... #}
<a class="button is-light" href="{{ path('app_login') }}">
	Connexion
</a>
{# ... #}

Authenticator

Lorsque ce formulaire sera envoyé, le service App\Security\FormLoginAuthenticator pourra intervenir. Ce dernier n'est pas encore complet.

Tout d'abord, il faut changer le nom du champ utilisé pour retrouver l'utilisateur. Il a été défini par défaut à email mais nous utiliserons username. Mettre à jour la méthode authenticate():

$username = $request->request->get('username', '');

$request->getSession()->set(Security::LAST_USERNAME, $username);

return new Passport(
	new UserBadge($username),
	new PasswordCredentials($request->request->get('password', '')),
	[
		new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
	]
);

Ce nouveau système d'authentification (disponible depuis Symfony 5.3) utilise un objet passeport qui contient toutes les informations nécessaires pour authentifier l'utilisateur. Il est nécessaire de passer un UserBadge contenant l'identifiant de l'utilisateur et un PasswordCredentials contenant le mot de passe. Les autres badges passés ensuite permettent d'activer diverses fonctionnalités.

Nous souhaitons pouvoir nous connecter en utilisant l'adresse email ou le pseudo de l'utilisateur. En l'état actuel, Symfony ira chercher l'utilisateur par son email uniquement.
Créer une méthode dans le UserRepository:

public function findOneByEmailOrPseudo(?string $userIdentifier): ?User
{
	return $this->createQueryBuilder('u')
		->where('u.email = :email')
		->orWhere('u.pseudo = :pseudo')
		->setParameters([
			'email' => $userIdentifier,
			'pseudo' => $userIdentifier,
		])
		->getQuery()
		->getOneOrNullResult()
	;
}

Injecter le UserRepository dans l'authenticator:

public function __construct(
	private readonly UrlGeneratorInterface $urlGenerator,
	private readonly UserRepository $userRepository,
) {
}

Puis passer cette méthode en 2e argument du UserBadge:

return new Passport(
	new UserBadge($username, $this->userRepository->findOneByEmailOrPseudo(...)),
	// ...
);

Perturbé par l'utilisation de ... ? Voir la documentation de la first class callable syntax.

Pour finaliser l'authenticator, il faut compléter la méthode onAuthenticationSuccess() dans laquelle effectuer une redirection vers la page de notre choix lorsque l'utilisateur a réussi à se connecter:

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
	if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
		return new RedirectResponse($targetPath);
	}

	 return new RedirectResponse($this->urlGenerator->generate('homepage'));
}

Configuration

Passons en revue la configuration de la sécurité avant de tester le formulaire de connexion. Ouvrir le fichier /config/packages/security.yaml.

Le nouveau système d'authentification est activé grâce à enable_authenticator_manager:

security:
    enable_authenticator_manager: true
	# ...

L'algorithme des mots de passe est géré automatiquement par le password hasher:

security:
    # ...
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # ...

Un user provider permet de gérer la récupération de nos entités User par leur identifiant (l'email):

security:
    # ...
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    # ...

Les firewalls gèrent les droits d'accès dans l'application. Le firewall dev désactive la sécurité dans les routes du profiler ou pour les fichiers statiques. Le firewall main concerne le reste de l'application et notre authenticator:

security:
    # ...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            custom_authenticator: App\Security\LoginFormAuthenticator
            logout:
                path: app_logout
	# ...

La clé logout pointe vers la route de notre SecurityController. C'est cette configuration qui va permettre d'intercepter la requête pour automatiquement déconnecter l'utilisateur. C'est pour cette raison que la méthode du controlleur ne sera jamais exécutée.
Ajouter une option target pour indiquer vers quelle route rediriger après la déconnexion:

security:
    # ...
    firewalls:
        # ...
        main:
            # ...
            logout:
			    # ...
                target: app_login

Test de connexion

Dans le navigateur, rendez vous sur la page de connexion. Dans la web debug toolbar, vous devez avoir un onglet concernant la sécurité avec une icone d'utilisateur indiquant "n/a" car nous ne sommes pas connecté.
Essayez de vous connecter avec de mauvais identifiants, un message d'erreur "Invalid Credentials" devrait apparaître.

Tester ensuite la connexion avec un utilisateur:

  • email: user1@mail.org
  • mot de passe: P@ssw0rd

Vous devriez être redirigés vers la page d'accueil et voir dans la WDT l'identifiant de l'utilisateur dans l'onglet de sécurité.
Pour vous déconnecter rapidement, survolez l'onglet et cliquez sur "Logout". Comme nous l'avons configuré précédemment, nous sommes redirigés sur la page de connexion.

Récupérez le pseudo de l'utilisateur:

php bin/console doctrine:query:sql "SELECT pseudo FROM \"user\" WHERE email = 'user1@mail.org'"

Puis tenter une connexion en utilisant le pseudo retourné au lieu de l'email. Le comportement doit être exactement le même que lors de la connexion précédente.