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.