Symfony

ESPACE MEMBRE AVEC SYMFONY

D

Dimanche 29 juillet 2018

Mis à jour le dimanche 29 juillet 2018

Espace membre avec Symfony

Vous souhaitez créer un espace membre dans votre application Symfony ?
Pas très compliqué.

Je suppose ici que vous avez déjà votre base de données et une table où vous stockez vos utilisateurs.

Votre UserProvider

Par exemple, vous avez déjà une entité User comme ceci :
<?php
// src/Entity/User.php
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

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

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $password;

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

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

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

        return $this;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

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

        return $this;
    }
}
Il suffit que vous implémentiez l'interface UserInterface (use Symfony\Component\Security\Core\User\UserInterface; - https://api.symfony.com/4.1/Symfony/Component/Security/Core/User/UserInterface.html) qui contient les méthodes getRoles(), getPassword(), getSalt(), getUsername() et eraseCredentials() à votre entité User, comme ceci :
<?php
// src/Entity/User.php
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

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

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

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $password;

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

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

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

        return $this;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

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

        return $this;
    }

    public function getUsername()
    {
        return $this->email;
    }

    public function getSalt()
    {
        return null;
    }
    
	// La méthode getPassword() est déjà implémentée
    //public function getPassword()
    //{
    //    return $this->password;
    //}

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function eraseCredentials()
    {
    }
}
Ensuite, il suffit de rajouter quelques lignes de configuration comme ceci :
# config/packages/security.yaml
security:
    encoders:
        App\Entity\User:
            algorithm: plaintext

    # ...

    providers:
        our_db_provider:
            entity:
                class: App\Entity\User
                property: username
                # if you're using multiple entity managers
                # manager_name: customer

    firewalls:
        main:
            anonymous: true
            form_login:
                login_path: login
                check_path: login

Configuration un peu plus avancée

Symfony propose d'implémenter l'interface AdvancedUserInterface (https://api.symfony.com/4.1/Symfony/Component/Security/Core/User/AdvancedUserInterface.html) qui permet d'ajouter des conditions afin d’autoriser un utilisateur à se connecter.
Cette interface implémente les méthodes isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired() et isEnabled(). Ces méthodes, comme leurs noms l'indiquent, permettent de refuser la connexion d'un utilisateur si son compte est respectivement expiré, bloqué, possède des identifiants expiré ou est désactivé.
Bien entendu, vous pouvez utilisez ces méthodes pour faire autre chose que ce dont elles ont été prévues pour. Par exemple, vous pouvez vérifier l'ip de l'utilisateur et renvoyer false à partir de la méthode isAccountNonExpired().
Il ne faut plus utiliser cette méthode qui est déprécié à partir de la version 4.1 de Symfony.

A la place, il faut utiliser la méthode des UserChecker qui permet de vérifier les mêmes conditions que l'interface AdvancedUserInterface et bien plus encore.
La documentation de Symfony explique très bien comment s'utilise cette méthode : https://symfony.com/doc/current/security/user_checkers.html.

Le formulaire de connexion

Créons un mini formulaire de connexion tout bête :
{# templates/login.html.twig #}
<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
<head>
	<meta charset="UTF-8">
	<title>Connexion</title>
</head>
<body>

<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1 id="connexion">Connexion</h1>
</div>

{% if error %}
    <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}

<form action="{{ path('login') }}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" value="{{ last_username }}" />

    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />

    {#
        If you want to control the URL the user
        is redirected to on success (more details below)
        <input type="hidden" name="_target_path" value="/account" />
    #}

    <button type="submit">login</button>
</form>

{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    <p>Connecté : Username: {{ app.user.username }}</p>
{% else %}
	déconnecté
{% endif %}

</body>
</html>

Le contrôleur

Créons un petit contrôleur qui va permettre de se connecter :
<?php
// src/Controller/AuthentificationController.php
namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class AuthentificationController extends Controller
{
    /**
     * @Route("/login", name="login")
     */
    public function login(Request $request, AuthenticationUtils $authenticationUtils)
    {
		
		// get the login error if there is one
		$error = $authenticationUtils->getLastAuthenticationError();

		// last username entered by the user
		$lastUsername = $authenticationUtils->getLastUsername();
		
        return $this->render('login.html.twig', [
            'last_username' => $lastUsername,
            'error'         => $error,
        ]);
    }
}

Le formulaire d'inscription

Dans notre entité User, on rajoute le champ plainPassword qui va contenir le mot de passe en brute.
On rajoute ceci :
<?php
// src/Entity/User.php
namespace App\Entity;

// ...
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User implements UserInterface
{
    // ...

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=4096)
     */
    private $plainPassword;

    // ...

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }
}
Ce qui nous donne en entier :
<?php
// src/Entity/User.php
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;

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

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

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $password;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=4096)
     */
    private $plainPassword;

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

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

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

        return $this;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

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

        return $this;
    }

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }

    public function getUsername()
    {
        return $this->email;
    }

    public function getSalt()
    {
        return null;
    }
    
	// La méthode getPassword() est déjà implémentée
    //public function getPassword()
    //{
    //    return $this->password;
    //}

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function eraseCredentials()
    {
    }
}

Le formulaire

<?php
// src/Form/UserType.php
namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
		$builder
            ->add('email', EmailType::class)
            ->add('plainPassword', RepeatedType::class, array(
                'type' => PasswordType::class,
                'first_options'  => array('label' => 'Password'),
                'second_options' => array('label' => 'Repeat Password'),
            ))
        ;
    }

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

Déconnexion

Suffit de rajouter une ligne de configuration comme ceci :
# config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            logout:
                path:   /logout
                target: /
Ainsi qu'un petit contrôleur vide sur la route du path que vous indiquez dans la configuration :
<?php
// src/Controller/AuthentificationController.php
namespace App\Controller;

// ...

class AuthentificationController extends Controller
{
    // ...
	
    /**
     * @Route("/logout", name="logout")
     */
    public function logout()
    {
    }
}
(Documentation Symfony : https://symfony.com/doc/current/security.html#logging-out)

Encoder les mots de passe

Pour encoder manuellement un mot de passe dans un contrôleur : https://symfony.com/doc/current/security/password_encoding.html.

Pour encoder les mots de passe qui sont actuellement en brute dans votre base de données : https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password.

Protéger les formulaires de connexion/inscription contre les failles CSRF

La documentation de Symfony est très claire et explicite à ce sujet, tout se trouve ici : https://symfony.com/doc/current/security/csrf.html.

Protection contre les longs mots de passe

De même, Symfony explique très bien la problématique dans ce sujet : https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form.
Il suffit de rajouter l'assertion @Assert\Length(max=4096) sur le champ plainPassword de votre entité User, comme ceci :
<?php
// src/Entity/User.php
namespace App\Entity;

// ...

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User implements UserInterface
{
    // ...

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=4096)
     */
    private $plainPassword;

    // ...
}
Et c'est tout.



Une erreur ? une question ? une critique ? une faute ? un conseil ? ou tout simplement un merci ?

Lâche ton commentaire