Encoder le mot de passe

Actuellement, notre mot de passe est persisté en base de donnée en clair.

Nous allons ajouter une nouvelle clé dans notre payload POST /users qu'on appeleraplainPassword sur laquelle on va envoyer notre mot de passe en clair, et on va l'enregistré en base sur l'attribut password

Pour réaliser cette action nous allons utiliser un DataPersister :

# config/services.yaml
    App\DataPersister\UserDataPersister:
        bind:
            $decorated: '@api_platform.doctrine.orm.data_persister'
<?php

namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;

final class UserDataPersister implements ContextAwareDataPersisterInterface
{
    private $decorated;

    public function __construct(ContextAwareDataPersisterInterface $decorated)
    {
        $this->decorated = $decorated;
    }

    public function supports($data, array $context = []): bool
    {
        return $this->decorated->supports($data, $context)
            && $data instanceof User;
    }

    public function persist($data, array $context = [])
    {
        $user = $this->decorated->persist($data, $context);

        return $user;
    }

    public function remove($data, array $context = [])
    {
        return $this->decorated->remove($data, $context);
    }
}

Pour le moment notre UserDataPersister ne fait rien de plus que persist ou remove. Nous allons l'utiliser pour rajouter le UserPasswordEncoderInterface de Symfony.

Ce que nous voulons faire c'est récupéré un mot de passe en clair et le stocké encrypté en base. Nous allons créer un nouvel attribut plainPassword qui ne sera pas stocké en base mais sur lequel on va se baser dans notre UserDataPersister.

from route to user with data persister

Dans notre entité User nous rajoutons l'attribut plainPassword et nous supprimons l'écriture et les assertions sur l'attribut password


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

    /**
     * @ORM\Column(type="string")
     * @Groups({"user:write"})
     * @Assert\NotBlank()
     * @Assert\Length(
     *       min = 8,
     *       max = 32,
     *       minMessage = "Your password must be at least {{ limit }} characters long",
     *       maxMessage = "Your password cannot be longer than {{ limit }} characters"
     *  )
     * @Assert\Regex(
     *      "/^.*(?=.{8,})((?=.*[!@#$%^&*()\-_=+{};:,<.>]){1})(?=.*\d)((?=.*[a-z]){1})((?=.*[A-Z]){1}).*$/",
     *      message = "Your password needs an uppercase, a lowercase, a digit and a special character"
     *  )
     */
    private $plainPassword;

// ...

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

    public function setPlainPassword(string $plainPassword): self
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }

Notez bien : nous persistons uniquement password avec @ORM\Column(type="string"). plainPassword nous sert à récupérer le mot de passe pour l'encoder dans notre persister. Maintenant si nous effectuons un POST /users

{
    "email": "{{$randomEmail}}",
    "plainPassword": "P@sswordS3cure"
}

Nous avons l'erreur suivante

{
    "@context": "/api/contexts/Error",
    "@type": "hydra:Error",
    "hydra:title": "An error occurred",
    "hydra:description": "An exception occurred while executing 'INSERT INTO user (email, roles, password) VALUES (?, ?, ?)' with params [\"Germaine86@yahoo.com\", \"[]\", null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'password' cannot be null",

Retournons dans le UserDataPersister. On vérifie que plainPassword est present. Si c'est le cas, alors on va écraser password avec un mot de passe encodé qui utilise encodePassword().

Voici la méthode persit décorée avec notre logique :

    public function persist($data, array $context = [])
    {
        if ($data->getId() === null) {
            if ($data->getPlainPassword()) {
                $data->setPassword(
                    $this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
                );
                $data->eraseCredentials();
            }

        }

        return $this->decorated->persist($data, $context);
    }

On regarde si notre id est null ça veut dire qu'on est dans la création d'un utilisateur.

Maintenant nous pouvons effectuer un POST /users

{
    "email": "{{$randomEmail}}",
    "plainPassword": "P@sswordS3cure"
}

Et notre nouvel utilisateur est bien persisté et le mot de passe enregistré est encodé. Pour le vérifier vous pouvez faire php bin/console doctrine:query:sql "SELECT * FROM user";

Rajoutons un peu de sécurité en retournant dans notre entité User et décommenter $this->plainPassword = null; de façon à avoir :

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

Puis revenez dans l'UserDataPersister et appeler cette méthode juste après avoir set le password :

    public function persist($data, array $context = [])
    {
        if ($data->getPlainPassword()) {
            $data->setPassword(
                $this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
            );
            $data->eraseCredentials();
        }

        return $this->decorated->persist($data, $context);
    }

Voici l'entité User et le UserDataPersister complet

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass=UserRepository::class)
 * @ApiResource(
 *      normalizationContext={"groups"={"user:read"}},
 *      denormalizationContext={"groups"={"user:write"}},
 * )
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"user:read"})
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     * @Groups({"user:read", "user:write"})
     * @Assert\NotBlank()
     * @Assert\Email()
     * @Assert\Length(max="180")
     */
    private $email;

    /**
     * @ORM\Column(type="json")
     * @Groups({"user:read"})
     */
    private $roles = [];

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

    /**
     * @Groups({"user:write"})
     * @Assert\NotBlank()
     * @Assert\Length(
     *       min = 8,
     *       max = 32,
     *       minMessage = "Your password must be at least {{ limit }} characters long",
     *       maxMessage = "Your password cannot be longer than {{ limit }} characters"
     *  )
     * @Assert\Regex(
     *      "/^.*(?=.{8,})((?=.*[!@#$%^&*()\-_=+{};:,<.>]){1})(?=.*\d)((?=.*[a-z]){1})((?=.*[A-Z]){1}).*$/",
     *      message = "Your password needs an uppercase, a lowercase, a digit and a special character"
     *  )
     */
    private $plainPassword;

    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 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 UserInterface
     */
    public function getPassword(): string
    {
        return (string) $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;
    }

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

    public function setPlainPassword(string $plainPassword): self
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }
}
<?php

namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

final class UserDataPersister implements ContextAwareDataPersisterInterface
{
    private $decorated;
    private $userPasswordEncoder;

    public function __construct(
        ContextAwareDataPersisterInterface $decorated,
        UserPasswordEncoderInterface $userPasswordEncoder
    ) {
        $this->decorated = $decorated;
        $this->userPasswordEncoder = $userPasswordEncoder;
    }

    public function supports($data, array $context = []): bool
    {
        return $this->decorated->supports($data, $context)
            && $data instanceof User;
    }

    public function persist($data, array $context = [])
    {
        if ($data->getPlainPassword()) {
            $data->setPassword(
                $this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
            );
            $data->eraseCredentials();
        }

        return $this->decorated->persist($data, $context);
    }

    public function remove($data, array $context = [])
    {
        return $this->decorated->remove($data, $context);
    }
}
prev next

Commentaires

Connectez-vous pour laisser un commentaire