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.
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