Les nouveautés dans le composant Sécurité de Symfony
Chaque nouvelle version de Symfony apporte son lot de nouveautés. 🥰 Mais la version 5.1 propose un nouveau système d’authentification et ce système change le comportement interne de la sécurité Symfony, pour le rendre extensible et plus compréhensible.
Dans cet article, je vous propose de faire un tour d’horizon de ce nouveau système et des autres nouvelles fonctionnalités.
3,2,1… Top à la vachette 🐮
Activons le nouveau système
💡Pour utiliser le nouveau système, il faut tout d’abord update le security.yaml comme ceci :
# config/packages/security.yaml
security:
enable_authenticator_manager: true
Qu'est ce qui change ? 📚
Le workflow d'authentification est simplifié : dans le nouveau système, il n’y a qu’un Listener AuthenticatorManagerListener
qui va passer la requête à un Authenticator manager AuthenticatorManager
fourni par Symfony ; puis l'Authenticator manager va résoudre les Authenticators et retourner une Response.
Voici un schéma extrait d'un article de Wouter sur le sujet.
Désormais, tout est relié à un seul concept et une seule interface. Le concept est celui d’Authenticators et l’interface est la suivante :
Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface
💡 L'Authenticator fonctionne de la même manière que Guard. Voici l'interface à implémenter pour créer un Authenticator :
namespace Symfony\Component\Security\Http\Authenticator;
interface AuthenticatorInterface
{
public function supports(Request $request): ?bool;
public function authenticate(Request $request): PassportInterface;
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface;
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response;
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response;
}
⚠️ Cette interface vient remplacer GuardAuthenticatorInterface
qui fut introduit dans la version Symfony 2.8 ; les méthodes getUser
et getCredentials
sont remplacées par une nouvelle méthode authenticate()
.
Pour rappel, avec Guards
la méthode getCredentials
passait les credentials
récupérés depuis une instance de Symfony\Component\HttpFoundation\Request
à la méthode getUser
, qui devait à son tour retourner un User
pour poursuivre le workflow de connexion. Désormais, cette tâche est déléguée à une méthode autenticate
qui doit retourner PassportInterface
.
PassportInterface
est une nouvelle notion : un Passeport est une classe qui va contenir les informations ayant besoin d’être validées durant le workflow d’authentification et ces informations seront transportées avec une nouvelle notion (sinon c’est pas drôle), qui est la notion de “Badge”, qui sert à ajouter des informations au passeport pour étendre la sécurité.
Dans le cas d’un Login soumis via un formulaire on aurait un Authenticator comme ceci :
class LoginAuthenticator implements AuthenticatorInterface
{
// ...
public function authenticate(Request $request): PassportInterface
{
$password = $request->request->get('password');
$username = $request->request->get('username');
return new Passport(
new UserBadge($email), // Badge pour transporter l'user
new PasswordCredentials($password), // Badge pour transporter le password
[new CsrfTokenBadge('login', $csrfToken)] // Badge pour transporter un token CSRF
);
}
}
Les badges en action 😎
UserBadge
PasswordCredentials
CsrfTokenBadge
sont des badges qui doivent implémenter une interface BadgeInterface.
. Cette interface a une méthode, isResolved
, et celle-ci doit retourner true
pour tous les badges pour que l’authentification réussisse.
Petite explication sur qui fait quoi ❓
UserBadge
va résoudre l’utilisateur via unProvider
défini dans la configuration ou uncallable
qu’on peut passer en deuxième argument du constructeur. 👤PasswordCredentials
va checker le password. 🔐CrsfTokenBadge
va checker que le token CRSF est valide. 🍪Passport
va se charger de transporter tout ça. ✈️
💝 Voici le code qui boucle sur les badges pour confirmer l’authentification :
namespace Symfony\Component\Security\Http\Authenticator\Passport;
class Passeport
{
// ...
public function checkIfCompletelyResolved(): void
{
// Dans notre exemple $this->badges contiens UserBadge,
// PasswordCredentials et CsrfTokenBadge
foreach ($this->badges as $badge) {
if (!$badge->isResolved()) {
throw new BadCredentialsException(
sprintf('Authentication failed security badge "%s" blabla)
);
}
}
}
}
Ce qui est vraiment pratique, c'est que vous pouvez ajouter vous-même des badges custom avec votre logique dans la méthode isResolved
👌.
Un système plus extensible ✨
La nouvelle interface AuthenticatorInterface
modifie aussi les arguments de la méthode createAuthenticatedToken.
Dans Guards
, on avait un UserInterface
et le firewall
dans les paramètres. Il était donc très difficile d’ajouter des informations custom au Token
créé.
Dans le nouveau système, on récupère le PassportInterface
retourné dans la méthode authenticate
, il y a donc beaucoup plus de contexte pour créer notre token 🎉.
class ApiAuthenticator implements AuthenticatorInterface
{
// ...
public function authenticate(Request $request): PassportInterface
{
$oauthContext = "any additional context";
$passport = new SelfValidatingPassport(new UserBadge($username), []);
// on ajoute du context dont on peux se servir
// dans createAuthenticatedToken
$passport->setAttribute('context', $oauthContext);
return $passport;
}
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
// récupère le contexte
$context = $passport->getAttribute('context');
return new CustomOauthToken($passport->getUser(), $context);
}
}
🔥 Si vous n'avez pas de besoins spécifiques, il est inutile de créer un Authenticator, Symfony met à disposition de nombreux authenticators :
Pour les utiliser, il suffit juste de déclarer celui qui vous voulez :
firewalls:
main:
form_login: ~ # FormLoginAuthenticator
# OR
http_basic: ~ # HttpBasicAuthenticato
🌠 Allons jusqu'à la version 5.2 (date d’écriture de l’article pour recenser d’autres nouveautés). Voici une liste non exhaustive :
- Des Events à gogo 🎁 (CheckPassportEvent, LoginSuccessEvent , LogoutEvent, SwitchUserEvent…)
- Fin de l'Anonymous User : soit l’utilisateur est authentifié soit non et dans ce cas il n’y a pas de token dans “la sécurité”
- Apparition de
PUBLIC_ACCESS
dans l’access control dusecurity.yaml
pour autoriser les utilisateurs non authentifiés l’équivalent du bon vieuxIS_AUTHENTICATED_ANONYMOUSLY
- Login Throttling pour limiter le nombre de tentatives de connexion
- Login Link pour authentifier un utilisateur via un lien (par email…)
- Accorder l’accès aux utilisateurs non authentifié dans un Voter Custom
Voilà les grosses nouveautés que j'ai pu relever. 😎
Merci de m'avoir lu n'hésitez pas à partager l'article si celui-ci vous a plu.
Smaïne Milianni
Sources : Pour écrire cet article je me suis appuyé de la documentation, d'un article de Wouter, des slides de la présentation faîtes par Ryan Weaver à la SFCon 2020 et la meilleure documentation étant le code. J’ai moi-même exploré ces nouveautés en ouvrant le vendor à coup de
ctrl+enter
🔦.