Cours - Symfony - Chapitre 07 - Protéger l'accès à une page

SOMMAIRE


Introduction


Dans le chapitre précédent, nous avons créé une page permettant d’afficher la liste des prises de contact. Cette page est destinée à l’administrateur du site et ne doit pas être consultée par d’autres personnes.


Symfony propose des outils permettant de gérer simplement et rapidement la sécurité.


Mise en place de la sécurité


Connectez-vous avec votre compte login et situez-vous dans le répertoire de votre projet.


su login4400


cd /var/www/html/share


Installation du package


composer require symfony/security-bundle


Mise en place du modèle (entité) 


Nous allons créer une nouvelle entité qui nous permettra d’enregistrer les utilisateurs du site. Pour l’instant, nous avons besoin uniquement d’un compte qui aura le rôle d’administrateur.


Symfony fournit un utilitaire qui permet la création d’une entité spéciale qui fait appel à « UserInterface », une interface qui propose de gérer la connexion, le mot de passe, etc.


Tapez la commande :


php bin/console make:user


Par défaut, il vous propose de créer une classe « User ». Appuyez sur « entrer » pour valider.


Nous allons stocker nos utilisateurs dans la base de données.


Il nous demande maintenant de sélectionner la donnée permettant de se connecter entre un email, un nom d’utilisateur ou un identifiant. Par défaut, il propose l’email, cela nous convient tout à fait.


Il propose de gérer le « hachage du mot de passe ». Nous acceptons.


La commande est terminée, il a créé l’entité « User » et le « UserRepository » qui correspond.


Voici l’entité créée que vous trouverez dans « src\Entity\User.php » :


<?php

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass=UserRepository::class)
 */
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    /**
     * @var string The hashed password
     * @ORM\Column(type="string")
     */
    private $password;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    /**
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUserIdentifier(): string
    {
        return (string) $this->email;
    }

    /**
     * @deprecated since Symfony 5.3, use getUserIdentifier instead
     */
    public function getUsername(): string
    {
        return (string) $this->email;
    }

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }
    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Returning a salt is only needed, if you are not using a modern
     * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
     *
     * @see UserInterface
     */
    public function getSalt(): ?string
    {
        return null;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }
}


Remarque :

Contrairement à la création d’une entité avec l’outil « make:entity », la commande « make:user » a permis d’ajouter les méthodes demandées par les interfaces « UserInterface » et « PasswordAuthenticatedUserInterface ». Vous pouvez les distinguer en lisant les annotations au-dessus des propriétés et des méthodes.


Dans « src\Repository\UserRepository.php » :


<?php

namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;

use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

/**
 * @method User|null find($id, $lockMode = null, $lockVersion = null)
 * @method User|null findOneBy(array $criteria, array $orderBy = null)
 * @method User[]    findAll()
 * @method User[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    /**
     * Used to upgrade (rehash) the user's password automatically over time.
     */
    public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
        }

        $user->setPassword($newHashedPassword);
        $this->_em->persist($user);
        $this->_em->flush();
    }

    // /**
    //  * @return User[] Returns an array of User objects
    //  */
    /*
    public function findByExampleField($value)
    {
        return $this->createQueryBuilder('u')
            ->andWhere('u.exampleField = :val')
            ->setParameter('val', $value)
            ->orderBy('u.id', 'ASC')
            ->setMaxResults(10)
            ->getQuery()
            ->getResult()
        ;
    }
    */

    /*
    public function findOneBySomeField($value): ?User
    {
        return $this->createQueryBuilder('u')
            ->andWhere('u.exampleField = :val')
            ->setParameter('val', $value)
            ->getQuery()
            ->getOneOrNullResult()
        ;
    }
    */
}


Remarque :

Le « repository » propose une méthode permettant de mettre à jour un mot de passe. Nous l’utiliserons dans quelques chapitres.

Cette nouvelle entité créée, nous devons l’intégrer à notre base de données.


Préparation de la migration 

php bin/console make:migration


Mise à jour de la base de données 

php bin/console doctrine:migrations:migrate



Nous constatons, dans le concepteur de PhpMyAdmin, la création de notre nouvelle table, « User ».


Création du formulaire de connexion

Nous allons demander à Symfony de créer notre formulaire de connexion, si ce n’est pas fait, installez le package nécessaire :


composer require symfony/maker-bundle --dev


puis utilisez la commande suivante :


php bin/console make:auth


Il nous demande de sélectionner le type d’authentification. Nous choisirons l’option 1, « Login form authenticator ».


Il nous propose le nom « AppCustomAuthenticator », vous pouvez choisir cette valeur ou la vôtre en respectant la casse, car il s’agit de créer une classe. (Majuscule pour commencer chaque mot).


Il nous demande maintenant de créer un contrôleur pour la sécurité et nous propose un nom par défaut. Appuyez sur la touche « Entrée » pour l’utiliser.


Il nous propose de créer la fonctionnalité permettant de se déconnecter avec l’url « /logout ». Répondez oui.


L’interaction avec la commande est maintenant terminée, voici les éléments qu’il a créés pour nous :

  • Le fichier « App\Security\AppCustomAuthenticator.php » qui nous permettra de personnaliser le comportement de la connexion par exemple.

  • Le contrôleur « SecurityController »que vous trouverez dans « src\controller ».

  • Le fichier « templates/security/login.html.twig » permettant d’afficher le formulaire de connexion.


Vous avez maintenant accès à une nouvelle route pour vous connecter. Il s’agit de l’url « /login », le nom de la route est « app_login ».


Le contrôleur « login » :


/**
     * @Route("/login", name="app_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // if ($this->getUser()) {
        //     return $this->redirectToRoute('target_path');
        // }

        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
    }


Remarque :

Si la connexion échoue, le contrôleur récupère l’erreur, l’affecte à la variable $error. Il récupère également l’email saisi lors de la tentative de connexion et l’affecte à la variable $lastUsername. Il passe ces 2 nouvelles variables à la vue (au fichier TWIG) afin qu’elle les affiche.


Le rendu du template « login.html.twig » :



Voici le code du template : (templates/security/login.html.twig)


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

{% block title %}Log in!{% endblock %}

{% block body %}
<form method="post">
    {% if error %}
        <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    {% if app.user %}
        <div class="mb-3">
            You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
        </div>
    {% endif %}

    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputEmail">Email</label>
    <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" autocomplete="email" required autofocus>
    <label for="inputPassword">Password</label>
 <input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required>

    <input type="hidden" name="_csrf_token"
           value="{{ csrf_token('authenticate') }}"
    >

    {#
        Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
        See https://symfony.com/doc/current/security/remember_me.html

        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" name="_remember_me"> Remember me
            </label>
        </div>
    #}

    <button class="btn btn-lg btn-primary" type="submit">
        Sign in
    </button>
</form>
{% endblock %}


Commentaires :


{% if error %} est une condition écrite dans le langage TWIG. Ici, elle teste si la variable « error » contient une valeur. Si c’est le cas, le code du bloc va afficher son contenu pour prévenir l’utilisateur. {{ error.messageKey|trans(error.messageData, 'security') }}

{% if app.user %} permet de tester si l’utilisateur est déjà authentifié. Si c’est le cas, elle affiche avec {{ app.user.username }} son identifiant de connexion.

<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"> permet à Symfony de sécuriser son formulaire. Nous verrons cela dans un autre chapitre, mais sachez que Symfony vous donne un formulaire en y cachant un token. Celui lui permet de savoir si c’est bien lui qui vous l’a donné et d’y mettre une date d’expiration. S’il a expiré, il sera nécessaire de recharger la page.

Vous pouvez mettre des commentaires dans TWIG en les entourant de {# #}


Nous allons mettre un peu en forme ce formulaire généré automatiquement afin de le rendre un peu plus présentable.



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

{% block title %}{{parent()}} Connectez-vous
{% endblock %}

{% block body %}

	<div class="container-fluid">
	<h1 class="text-center text-primary mt-4 pt-4 display-1 fw-bold">
			Connectez-vous</h1>


		<div class="row justify-content-center">
			<div class="col-12 col-md-6 bg-white p-4 m-0 text-primary">

				<form method="post">
					{% if error %}
						<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
					{% endif %}

					{% if app.user %}
						<div class="mb-3">
							You are logged in as
							{{ app.user.username }},
							<a href="{{ path('app_logout') }}">Logout</a>
						</div>
					{% endif %}

					
					<label for="inputEmail" class="fw-bold">Email</label>
					<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" autocomplete="email" required autofocus>
					<label for="inputPassword" class="fw-bold">Mot de passe</label>
					<input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required>

					<input
					type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

					{#
					        Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
					        See https://symfony.com/doc/current/security/remember_me.html
					
					        <div class="checkbox mb-3">
					            <label>
					                <input type="checkbox" name="_remember_me"> Remember me
					            </label>
					        </div>
					    #}
                    <div class="text-center">
					<button class="btn bg-primary text-white m-4" type="submit">
						CONNEXION
					</button>
                    </div>
				</form>
			</div>
		</div>
	</div>
{% endblock %}


Nous avons maintenant intégré notre formulaire de connexion. Cependant, pour le tester, nous avons besoin d’utilisateurs inscrits.


Création du formulaire d’inscription

Installation du composant permettant d’écrire des règles de validation pour les formulaires :


composer require validator


Création du formulaire :


php bin/console make:registration-form


La première étape vous demande si vous souhaitez qu’un compte soit unique dans votre base de données, dans notre cas, il exigera un email qui n’existe pas déjà dans la base. Répondez oui.

Il demande ensuite si nous souhaitons envoyer un email de confirmation après l’inscription. Répondez oui.

Par défaut, l’utilisateur recevra l’email et pourra confirmer son inscription en cliquant sur son email uniquement s’il est identifié. Vous pouvez supprimer l’obligation d’être connecté afin de pouvoir facilement confirmer votre inscription à partir d’un autre appareil. Pour cela, il vous demande s’il intègre un identifiant d’utilisateur dans l’email. Répondez oui.


Donnez ensuite l’email qui permettra d’envoyer la confirmation, puis le nom qui s’affichera.


Voulez-vous connecter la personne automatiquement après la validation. Répondez non.

Choisissez ensuite la route sur laquelle sera redirigée la personne confirmant son inscription. Je mets le numéro de la route « app_login ».


La commande est enfin terminée. Nous devons maintenant compléter les éléments manquants.


Installation du package permettant d’envoyer un email de confirmation :


composer require symfonycasts/verify-email-bundle


Voici le contenu du nouveau contrôleur « src/Controller/RegistrationController.php » :


<?php

namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;

use App\Repository\UserRepository;
use App\Security\EmailVerifier;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;

class RegistrationController extends AbstractController
{
    private EmailVerifier $emailVerifier;

    public function __construct(EmailVerifier $emailVerifier)
    {
        $this->emailVerifier = $emailVerifier;
    }

    #[Route('/register', name: 'app_register')]
    public function register(Request $request, UserPasswordHasherInterface $userPasswordHasherInterface): Response
    {
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // encode the plain password
            $user->setPassword(
            $userPasswordHasherInterface->hashPassword(
                    $user,
                    $form->get('plainPassword')->getData()
                )
            );

            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($user);
            $entityManager->flush();

            // generate a signed url and email it to the user
            $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
                (new TemplatedEmail())
                    ->from(new Address('contact@nuage-pedagogique.fr', 'Share Admin'))
                    ->to($user->getEmail())
                    ->subject('Please Confirm your Email')
                    ->htmlTemplate('registration/confirmation_email.html.twig')
            );
            // do anything else you need here, like send an email

            return $this->redirectToRoute('app_login');

    }

        return $this->render('registration/register.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }

    #[Route('/verify/email', name: 'app_verify_email')]
    public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
    {
        $id = $request->get('id');

        if (null === $id) {
            return $this->redirectToRoute('app_register');
        }

        $user = $userRepository->find($id);

        if (null === $user) {
            return $this->redirectToRoute('app_register');
        }

        // validate email confirmation link, sets User::isVerified=true and persists
        try {
            $this->emailVerifier->handleEmailConfirmation($request, $user);
        } catch (VerifyEmailExceptionInterface $exception) {
            $this->addFlash('verify_email_error', $exception->getReason());

            return $this->redirectToRoute('app_register');
        }

        // @TODO Change the redirect on success and handle or remove the flash message in your templates
        $this->addFlash('success', 'Your email address has been verified.');

        return $this->redirectToRoute('app_register');
    }
}


Commentaires :

UserPasswordHasherInterface $userPasswordHasherInterface // cette variable servira à encoder le mot de passe avec sa méthode «  hashPassword ».

$form->get('plainPassword')->getData() // permet de récupérer le mot de passe saisi dans le formulaire.

$userPasswordHasherInterface->hashPassword($user, $form->get('plainPassword')->getData())); // la méthode qui hache le mot de passe a besoin de 2 paramètres : l’utilisateur et le mot de passe du formulaire.

$id = $request->get('id'); // récupère la valeur « id » passée dans l’url.

if (null === $id) { return $this->redirectToRoute('app_register'); } // Si l’id est manquant nous redirigeons vers le formulaire d’inscription.

if (null === $user) // si l’utilisateur n’existe pas dans la base de données, nous le renvoyons également sur la page d’inscription.

try { // Lance la vérification, s’il y a une erreur, nous la récupérons afin de la gérer dans le catch et envoyer l’utilisateur sur le formulaire d’inscription.

$this->emailVerifier->handleEmailConfirmation($request, $user);

} catch (VerifyEmailExceptionInterface $exception) {

$this->addFlash('verify_email_error', $exception->getReason());

return $this->redirectToRoute('app_register');

}


Il a créé un formulaire permettant de vous inscrire. Vous le retrouverez dans « src/Form/RegistrationFormType.php ».


<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('email')
            ->add('agreeTerms', CheckboxType::class, [
                'mapped' => false,
                'constraints' => [
                    new IsTrue([
                        'message' => 'You should agree to our terms.',
                    ]),
                ],
            ])
            ->add('plainPassword', PasswordType::class, [
                // instead of being set onto the object directly,
                // this is read and encoded in the controller
                'mapped' => false,
                'attr' => ['autocomplete' => 'new-password'],
                'constraints' => [
                    new NotBlank([
                        'message' => 'Please enter a password',
                    ]),
                    new Length([
                        'min' => 6,
                        'minMessage' => 'Your password should be at least {{ limit }} characters',
                        // max length allowed by Symfony for security reasons

                       'max' => 4096,
                    ]),
                ],
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}


Commentaires :

Le formulaire est créé avec un composant supplémentaire non relié à l’entité « User ». Il s’agit de

«  agreeTerms », une case à cocher demandant l’accord de l’utilisateur pour s’inscrire selon les termes du site.

'mapped' => false // indique au formulaire que ce n’est pas une donnée de l’entité « User ».

'constraints' => [ // création d’une contrainte sur la case à cocher

new IsTrue // nous instancions une contrainte « IsTrue » qui impose que la case soit cochée

'message' => 'You should agree to our terms.', // Message afin d’indiquer à l’utilisateur ce que vous attendez de lui.

]),

],

NotBlank // le composant impose la saisie d’une donnée.

Length // impose une condition sur la longueur d’une chaine de caractères. « Min » permet de donner le nombre minimum de caractères.


Enfin, la vue de l’interface d’inscription se trouve dans « templates/registration/register.html.twig » :


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

{% block title %}Register{% endblock %}

{% block body %}
    {% for flashError in app.flashes('verify_email_error') %}
        <div class="alert alert-danger" role="alert">{{ flashError }}</div>
    {% endfor %}

    <h1>Register</h1>

    {{ form_start(registrationForm) }}
        {{ form_row(registrationForm.email) }}
        {{ form_row(registrationForm.plainPassword, {
            label: 'Password'
        }) }}
        {{ form_row(registrationForm.agreeTerms) }}

        <button type="submit" class="btn">Register</button>
    {{ form_end(registrationForm) }}
{% endblock %}


L’email de confirmation se trouve dans le template à cette adresse « template/registration/confirmation_email.html.twig ».


<h1>Hi! Please confirm your email!</h1>

<p>
    Please confirm your email address by clicking the following link: <br><br>
    <a href="{{ signedUrl }}">Confirm my Email</a>.
    This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}.
</p>

<p>
    Cheers!
</p>


L’envoi de l’email se fait à l’aide de la classe « EmailVerifier » qui se situe dans « src/Security/EmailVerifier.php ».


Il a ajouté le champ supplémentaire dans l’entité « User » afin de vérifier si l’inscription a été confirmée.


 private $isVerified = false;


Par conséquent, vous devez mettre à jour la base de données avec les 2 commandes suivantes :


php bin/console make:migration
php bin/console doctrine:migrations:migrate



Testons l’inscription générée par Symfony 


L’url générée par défaut est « /register ».



Voici l’email reçu :






#[Route('/verify/email', name: 'app_verify_email')]
    public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
    {
        $id = $request->get('id');

        if (null === $id) {
            return $this->redirectToRoute('app_register');
        }

        $user = $userRepository->find($id);

        if (null === $user) {
            return $this->redirectToRoute('app_register');
        }

        // validate email confirmation link, sets User::isVerified=true and persists
        try {
            $this->emailVerifier->handleEmailConfirmation($request, $user);
        } catch (VerifyEmailExceptionInterface $exception) {
            $this->addFlash('verify_email_error', $exception->getReason());

            return $this->redirectToRoute('app_register');
        }

        // @TODO Change the redirect on success and handle or remove the flash message in your templates
        $this->addFlash('notice', 'Votre adresse est maintenant vérifiée.');

        return $this->redirectToRoute('app_login');
    }


Voici un exemple de la table user lorsque vous n’avez pas encore validé votre inscription :



Nous remarquons que le compte n’a pas été vérifié, car le champ « is_verified » est encore à 0.


Voici l’affichage que j’obtiens lorsque je clique sur l’email du lien.



J’ai la possibilité de saisir directement mes identifiants afin de me connecter. Sauf que cela ne fonctionne pas, nous devons ajouter une petite méthode avant.


Dans « AppCustomAuthenticator.php », ajoutez la méthode « supports » :


<?php

namespace App\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class AppCustomAuthenticator extends AbstractLoginFormAuthenticator
{
    use TargetPathTrait;

    public const LOGIN_ROUTE = 'app_login';

    private UrlGeneratorInterface $urlGenerator;

    public function __construct(UrlGeneratorInterface $urlGenerator)
    {
        $this->urlGenerator = $urlGenerator;
    }

    public function authenticate(Request $request): PassportInterface
    {
        $email = $request->request->get('email', '');

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

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

    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('index'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

    protected function getLoginUrl(Request $request): string
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }

    public function supports(Request $request): bool
    {
        return self::LOGIN_ROUTE === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }
}



Vous pouvez maintenant vous connecter. Si vos identifiants sont corrects, le site vous redirigera vers la page d’accueil, dans le cas contraire, il vous affichera le formulaire de connexion avec une erreur.



Récupération des données de connexion dans TWIG

Lorsque vous êtes connecté, vous n’avez aucune information à l’écran pour vous l’indiquer.

Normalement, si vous n’êtes pas connecté, vous devriez voir les liens « se connecter » et « s’inscrire » dans le menu.


Pour cela, allez dans le template « templates/base.html.twig » puis faites les modifications suivantes :


<!doctype html>
<html lang="fr">
	<head>
		<!-- Required meta tags -->
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		{% block stylesheets %}
			<!-- Bootstrap CSS -->
			<link href="https://bootswatch.com/5/lumen/bootstrap.min.css" rel="stylesheet">
			<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
		{% endblock %}
		<title>
			{% block title %}Share -
			{% endblock %}
		</title>
	</head>
	<body>


		<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
			<div class="container-fluid">
				<a class="navbar-brand " href="{{path('index')}}">SHARE</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>

				<div class="collapse navbar-collapse" id="navbarColor01">
					<ul class="navbar-nav me-auto">
						<li class="nav-item">
							<a class="nav-link text-white" href="{{path('index')}}">Accueil
							</a>
						</li>
						<li class="nav-item">
							<a class="nav-link text-white" href="{{path('contact')}}">Contact
							</a>
						</li>
						<li class="nav-item">
							<a class="nav-link text-white" href="{{path('apropos')}}">À propos
							</a>
</li>
						<li class="nav-item">
							<a class="nav-link text-white" href="{{path('mentions')}}">Mentions légales
							</a>
						</li>
						{% if not is_granted('IS_AUTHENTICATED_FULLY') %}
							<li class="nav-item">
								<a class="nav-link text-white" href="{{path('app_login')}}">Se connecter
								</a>
							</li>
							<li class="nav-item">
								<a class="nav-link text-white" href="{{path('app_register')}}">S'inscrire
								</a>
							</li>
						{% else %}
							<li class="nav-item">
								<a class="nav-link" href="{{path('app_logout')}}">
									<i class="bi bi-x-circle-fill text-white"></i>
								</a>
							</li>
						{% endif %}
					</ul>
				</div>
			</div>
		</nav>

		{% for message in app.flashes('notice') %}
			<h2 class="alert alert-warning text-center mt-4 mb-4" role="alert">
				{{ message }}
			</h2>
		{% endfor %}

		{% block body %}{% endblock %}

		{% block javascripts %}
			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
		{% endblock %}
	</body>
</html>


Commentaire :


{% if not is_granted('IS_AUTHENTICATED_FULLY') %} // Permet d’afficher des éléments lorsqu’un utilisateur n’est pas du tout connecté.


https://icons.getbootstrap.com est un site sur lequel vous trouverez des icons pour votre site.


Voici le rendu de la page si vous n’êtes pas connecté :



Et voici la page lorsque vous êtes connecté :



Il serait intéressant d’afficher également l’adresse email de la personne qui est connectée.


Voici la modification à effectuer dans le menu du fichier « templates/base.html.twig » :


{% else %}
		<li class="nav-item">
	        <a class="nav-link text-white" href="{{path('app_logout')}}"> {{app.user.email}}
				<i class="bi bi-x-circle-fill text-white"></i>
			</a>
		</li>
{% endif %}


Commentaire :


La variable « app » contient les données de l’application. Elle comprend « user » qui permet de retrouver toutes les données de l’utilisateur connecté.


Et voici l’affichage correspondant :




Protéger l’accès d’une page

Dans le chapitre précédent, nous avons créé une liste des prises de contact envoyées via le formulaire. Nous allons maintenant protéger l’accès à l’url « liste-contacts « . Seule une personne connectée pourra consulter cette liste. La protection est relative puisque nous n’avons pas donné un rôle précis à notre nouvel utilisateur. Ainsi, une fois inscrit, n’importe quel utilisateur pourra consulter nos messages. Nous protègerons davantage notre site dans le prochain chapitre.




Travail à faire :


Ajoutez dans le menu, un accès à la page liste-contacts du chapitre précédent uniquement si la personne est connectée.

Protection de pages en se basant sur l’url 


Dans le fichier « src/controller/ContactController.php », modifiez l’url de la route. Nous allons ajouter « private » au début de l’url.


#[Route('/private-liste-contacts', name: 'liste-contacts')]
    public function listeContacts(): Response
    {
        $repoContact = $this->getDoctrine()->getRepository(Contact::class);
        $contacts = $repoContact->findAll();
        return $this->render('contact/liste-contacts.html.twig', [
           'contacts' => $contacts
        ]);
    }


Dans le fichier « config/packages/security.yaml », créez la ligne suivante à la fin :


- { path: ^/private, roles: IS_AUTHENTICATED_FULLY }


Voici le fichier complet :


security:
    enable_authenticator_manager: true
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
        App\Entity\User:
            algorithm: auto

    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            custom_authenticator: App\Security\AppCustomAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                # target: app_any_route

            # activate different ways to authenticate
  # https://symfony.com/doc/current/security.html#the-firewall

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }
        - { path: ^/private, roles: IS_AUTHENTICATED_FULLY }


Commentaires :

password_hashers: // rubrique permettant de configurer le hachage de l’application

providers: // rubrique permettant d’indiquer où sont stockées les données de connexion. « property » précise le login à utiliser.

- { path: ^/private, roles: IS_AUTHENTICATED_FULLY } // l’accès à une page dont l’url commence par /private sera autorisé uniquement à une personne connectée correctement.


Maintenant, déconnectez-vous du site et tentez de vous rendre sur cette url « /private-liste-contacts ». Vous verrez apparaitre le formulaire de connexion.


Nous allons nous arrêter sur cette étape et nous reprendrons la découverte de la sécurité avec Symfony dans le prochain chapitre.


Travail supplémentaire à réaliser :


Mettez en forme le formulaire d’inscription ainsi que l’email envoyé pour la confirmation.






Voir le Chapitre 8