Authentification élémentaire en Symfony

Nous allons ici présenter comment faire une authentification « à la main » en Symfony. Il existe d’autres méthodes plus éprouvées de réaliser cette fonctionnalité mais il est bon dans sa progression en PHP/Symfony de passer par cette étape.

Initialisation du projet Symfony

Tout d’abord création d’un nouveau projet Symfony (code pour la v5 de Symfony)

symfony new --full my_project
cd my_project

Ensuite nous allons créer une entité utilisateur grâce au makerbundle de Symfony

php bin/console make:entity

Les attributs de base pour notre utilisateur sont pseudo, motDePasse et role. Ce dernier nous permettra de définir si l’utilisateur dispose de droits spéciaux (ex : admin). Vous pouvez bien evidemment ajouter tous les attributs nécessaires à votre application.

_images/makeentityUtilisateur.jpeg

Enfin nous créons un CRUD pour notre entité Utilisateur

php bin/console make:crud
Cette commande nous permet d’avoir tous les outils nécessaires pour la bonne gestion de nos utilisateurs :
  • un controller implémentant les fonctionnalités nécessaires

  • l’utilisation des formulaires Symfony

  • des templates qui ne demandent qu’à être modifiés

Bien sûr nous pourrions tout faire à la main, mais Symfony nous permet de gagner du temps. Faites la migration.

Modification de notre application

L’application telle que nous l’avons, ne nous permet pas de gérer l’authentification de manière adéquate. Pour se faire, il va nous falloir apporter quelques modifications.

La création d’un utilisateur

Pour commencer, la méthode new :

<?php
    /**
     * @Route("/new", name="utilisateur_new", methods={"GET","POST"})
     */
    public function new(Request $request, UtilisateurRepository $utilisateurRepository): Response
    {
        $utilisateur = new Utilisateur();
        $form = $this->createForm(UtilisateurType::class, $utilisateur);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            //est ce que le pseudo est unique
            //utilisation d'une méthode que nous allons ajouter au repository
            if($utilisateurRepository->findOneByPseudo($utilisateur->getPseudo()) != null)
            {// s'il ne l'est pas, on renvoie vers la page de création d'utilisateur avec une notification
                return $this->render('utilisateur/new.html.twig', [
                    'utilisateur' => $utilisateur,
                    'form'        => $form->createView(),
                    'message'     => "Ce pseudo est déjà utilisé, merci d'en changer"
                ]);
            }
            $entityManager = $this->getDoctrine()->getManager();
            //Nous modifions l'utilisateur
            $pass = password_hash($utilisateur->getMotDePasse(), PASSWORD_DEFAULT);
            $utilisateur->setMotDePasse($pass);//mot de passe crypté
            $utilisateur->setRole("Utilisateur");//par défaut un nouvel utilisateur aura un role classique
            $entityManager->persist($utilisateur);
            $entityManager->flush();

            return $this->redirectToRoute('index');//on ne veut pas que l'utilisateur ai accès à la liste de tous les autres utilisateurs
        }

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

A la base, cette méthode ajoute un utilisateur, mais nous souhaitons tout d’abord que le pseudo soit unique, ensuite que le mot de passe soit chiffré

Danger

/!\ NE JAMAIS STOCKER DE MOT DE PASSE EN CLAIR /!\

et enfin attribuer un role par défaut.

Création de la méthode findOneByPseudo dans UtilisateurRepository

<?php
    /**
     * Cette méthode va nous permettre de récupérer un utilisateur via son pseudo
     */
    public function findOneByPseudo($value): ?Utilisateur
    {
        return $this->createQueryBuilder('u')
            ->andWhere('u.pseudo = :val')
            ->setParameter('val', $value)
            ->getQuery()
            ->getOneOrNullResult()
        ;
    }

Avant de tester, nous allons créer une route index, puis une <div> dans notre template de création dans le cas où le pseudo est déjà utilisé.

Route index :

<?php
    /**
     * @Route("/", name="index", methods={"GET"})
     * Nous la modifierons plus tard
     */
    public function page_index(UtilisateurRepository $utilisateurRepository): Response
    {
        return $this->render('utilisateur/index.html.twig', [
            'utilisateurs' => $utilisateurRepository->findAll(),
        ]);
    }

Base template :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}
        <link rel="stylesheet" href="https://bootswatch.com/4/yeti/bootstrap.min.css">
        {% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

On pourra utiliser les classes Bootstrap pour faire un CSS agréable et facile à mettre en place.

Template new :

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

{% block title %}New Utilisateur{% endblock %}

{% block body %}
    <h1>Create new Utilisateur</h1>

    {# nous testons si la variable message existe #}
    {% if message is defined %}
        <div class="alert alert-danger">
            {{ message }}
        </div>
    {% endif %}

    {{ include('utilisateur/_form.html.twig') }}

    <a href="{{ path('utilisateur_index') }}">back to list</a>
{% endblock %}
Nous pouvons maintenant tester notre modification :

Une fois que tout est vérifié, passons à la suite.

Le mot de passe apparait en clair dans le formulaire et nous devons mettre en place un test sur le mot de passe en le faisant saisir une seconde fois.

Cacher le mot de passe (/Form/UtilisateurType) :

<?php

    namespace App\Form;

    use App\Entity\Utilisateur;
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    //ajout du use pour utiliser le type input password de Symfony
    use Symfony\Component\Form\Extension\Core\Type\PasswordType;

    class UtilisateurType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('pseudo')
                ->add('motDePasse', PasswordType::class)//input type=text devient input type=password
                //suppression de la possibilité d'ajouter un rôle
            ;
        }

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

Nous modifions directement dans le formbuilder, cela permettra à notre modification de se propager dans toute l’application.

Nous modifions maintenant le template du new afin d’ajouter l’input de vérification du mot de passe :

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

{% block title %}New Utilisateur{% endblock %}

{% block body %}
    <h1>Create new Utilisateur</h1>

    {# nous testons si la variable message existe #}
    {% if message is defined %}
        <div class="alert alert-danger">
            {{ message }}
        </div>
    {% endif %}

    {{ form_start(form, {'attr': {'id': 'new_edit_utilisateur'}}) }}
    {# utilisation de classes bootstrap pour la mise en forme #}
        <div class="row">
            <div class="col-12">
                {{ form_label(form.pseudo) }}
                {{ form_widget(form.pseudo) }}
            </div>
            <div class="col-12">
                {{ form_label(form.motDePasse) }}
                {{ form_widget(form.motDePasse) }}
            </div>
            <div class="col-12">
                <label for="verifpass">Saisir une seconde fois le mot de passe</label>
                <input type="password" id="verifpass" required>
            </div>
        </div>
        <button class="btn btn-success">{{ button_label|default('Save') }}</button>
    {{ form_end(form) }}

    <a href="{{ path('utilisateur_index') }}">back to list</a>
{% endblock %}

Création d’un script JS avec jQuery afin de tester si les deux mot de passe sont identiques. Créez tout d’abord un dossier js dans public puis dans ce dossier, un fichier script.js.

Script de vérification :

$("#new_edit_utilisateur").on('submit', function(){
    if($("#utilisateur_motDePasse").val() != $("#verifpass").val()) {
        //implémntez votre code
        alert("Les deux mots de passe saisies sont différents");
        alert("Merci de renouveler l'opération");
        return false;
    }
})

Il ne faut pas oublier d’intégrer le lien vers jQuery et notre script.js dans le base template :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}
        <link rel="stylesheet" href="https://bootswatch.com/4/yeti/bootstrap.min.css">
        {% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}
            <script
                src="https://code.jquery.com/jquery-3.5.1.min.js"
                integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
                crossorigin="anonymous">
            </script>
            <script src="/js/script.js"></script>
        {% endblock %}
    </body>
</html>

Nous avons fini la modification pour la création d’un utilisateur.

Création de la page de connexion

Maintenant que nous avons le code pour créer l’utilisateur, nous allons passer à la partie concernant la connexion.

Template de connexion

Créer pour cela un nouveau template connexion.html.twig dans template/utilisateur :

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

{% block title %}Connexion{% endblock %}

{% block body %}
    <h1>Se connecter</h1>

    {# nous testons si la variable message existe #}
    {% if message is defined %}
        <div class="alert alert-danger">
            {{ message }}
        </div>
    {% endif %}

    {{ include('utilisateur/_form.html.twig', {'button_label': 'Se connecter'}) }}

    <a href="{{ path('utilisateur_index') }}">back to list</a>
{% endblock %}

Côté controller

<?php
    /**
    * @Route("/connexion", name="connexion", methods={"GET", "POST"})
    */
    public function connexion(Request $request, UtilisateurRepository $utilisateurRepository, Session $session): Response
    {
        //en cas de connexion ouverte
        if($session->has('user'))
        {
            //on la referme, ain de pouvoir initier une nouvelle connexion
            $session->remove('user');
        }

        $utilisateur = new Utilisateur();
        $form = $this->createForm(UtilisateurType::class, $utilisateur);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $pseudo      = $utilisateur->getPseudo();
            //on récupère le code crypté
            $passHash    = $utilisateurRepository->findOneByPseudo($pseudo)->getMotDePasse() ?? "pas d'utilisateur";
            //cette méthode vérifie que le mot de passe saisie et le hash correspondent
            $password    = password_verify($utilisateur->getMotDePasse(), $passHash);
            if($password)
            {
                $utilisateur = $utilisateurRepository->findOneByPseudo($pseudo);
                //on ouvre la connexion
                $session->set('user', $utilisateur);
                return $this->redirectToRoute('index');
            }

            return $this->render('utilisateur/connexion.html.twig', [
                'form'    => $form->createView(),
                'message' => "Connexion refusée"
                ]);
        }
            return $this->render('utilisateur/connexion.html.twig', [
            'form'    => $form->createView()
            ]);
    }

Nous utilisons l’objet Session de HttpFoundation, cela va nous permettre de maintenir ouverte une session et donc de savoir si un utilisateur est connecté. Ne pas oublier use Symfony\Component\HttpFoundation\Session\Session; dans le controller.

Pour des raisons de facilité, nous fermons la connexion dès que l’utilisateur souhaite accéder à la page de connexion. En temps normal nous préfererons un bouton qui permet à l’utilisateur de fermer sa connexion. Le but de ce tuto n’est pas de tout faire, mais juste d’exposer les principes et la manière d’y arriver.

A vous de vous appuyer sur ce tuto pour développer votre propre interface.

Table des matières

Sujet précédent

Consultation de l’API avec JS : fetch, await, async

Sujet suivant

Authentification avec le SecurityBundle de Symfony