I'm trying to put together a change password feature in Symfony2. I have a "current password" field, a "new password" field and a "confirm new password" field, and the part I'm currently focusing on is validating the "current password" field.
(By the way, I realize now that things like FOSUserBundle exist that would take care of a lot of these things for me, but I already built my authentication system based on the official Symfony documentation, and I don't have time right now to redo all my authentication code.)
What I'm imagining/hoping I can do is create a validation callback that says something like this:
// Entity/User.php
public function currentPasswordIsValid(ExecutionContext $context)
{
$currentPassword = $whatever; // whatever the user submitted as their current password
$factory = $this->get('security.encoder_factory'); // Getting the factory this way doesn't work in this context.
$encoder = $factory->getEncoder($this);
$encryptedCurrentPassword = $encoder->encodePassword($this->getPassword(), $this->getSalt());
if ($encyptedCurrentPassword != $this->getPassword() {
$context->addViolation('Current password is not valid', array(), null);
}
}
As you can see in my comments, there are at least a couple reasons why the above code doesn't work. I would just post specific questions about those particular issues, but maybe I'm barking up the wrong tree altogether. That's why I'm asking the overall question.
So, how can I validate a user's password?
There's a built-in constraint for that since Symfony 2.1.
First, you should create a custom validation constraint. You can register the validator as a service and inject whatever you need in it.
Second, since you probably don't want to add a field for the current password to the User class just to stick the constraint to it, you could use what is called a form model. Essentially, you create a class in the Form\Model namespace that holds the current password field and a reference to the user object. You can stick your custom constraint to that password field then. Then you create your password change form type against this form model.
Here's an example of a constraint from one of my projects:
<?php
namespace Vendor\Bundle\AppBundle\Validator\Constraints\User;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class CurrentPassword extends Constraint
{
public $message = "Your current password is not valid";
/**
* #return string
*/
public function validatedBy()
{
return 'user.validator.current_password';
}
}
And its validator:
<?php
namespace Vendor\Bundle\AppBundle\Validator\Constraints\User;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use JMS\DiExtraBundle\Annotation\Validator;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Inject;
/**
* #Validator("user.validator.current_password")
*/
class CurrentPasswordValidator extends ConstraintValidator
{
/**
* #var EncoderFactoryInterface
*/
private $encoderFactory;
/**
* #var SecurityContextInterface
*/
private $securityContext;
/**
* #InjectParams({
* "encoderFactory" = #Inject("security.encoder_factory"),
* "securityContext" = #Inject("security.context")
* })
*
* #param EncoderFactoryInterface $encoderFactory
* #param SecurityContextInterface $securityContext
*/
public function __construct(EncoderFactoryInterface $encoderFactory,
SecurityContextInterface $securityContext)
{
$this->encoderFactory = $encoderFactory;
$this->securityContext = $securityContext;
}
/**
* #param string $currentPassword
* #param Constraint $constraint
* #return boolean
*/
public function isValid($currentPassword, Constraint $constraint)
{
$currentUser = $this->securityContext->getToken()->getUser();
$encoder = $this->encoderFactory->getEncoder($currentUser);
$isValid = $encoder->isPasswordValid(
$currentUser->getPassword(), $currentPassword, null
);
if (!$isValid) {
$this->setMessage($constraint->message);
return false;
}
return true;
}
}
I use my Blofwish password encoder bundle, so I don't pass salt as the third argument to the $encoder->isPasswordValid() method, but I think you'll be able to adapt this example to your needs yourself.
Also, I'm using JMSDiExtraBundle to simplify development, but you can of course use the classical service container configuration way.
In Symfony 2.1 you can use the built-in validator:
http://symfony.com/doc/master/reference/constraints/UserPassword.html
So for instance in your form builder:
// declare
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
// mapped=>false (new in 2.1) is to let the builder know this is not an entity field
->add('currentpassword', 'password', array('label'=>'Current password', 'mapped' => false, 'constraints' => new UserPassword()))
Apparently there's a bug right now with that validator so might or might now work
https://github.com/symfony/symfony/issues/5460
FOSUserBundle uses a ModelManager class which is separate from the base Model. You can check their implementation.
I ended up cutting the Gordian knot. I bypassed all of Symfony's form stuff and did all the logic in the controller.
Related
I am using API Platform and I followed this tutorial to add a custom serialized field which relies on an external service. The avatar property needs to be exposed using the Packages class.
<?php
namespace App\Serializer;
use App\Entity\User;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\UrlHelper;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class UserNormalizer implements ContextAwareNormalizerInterface
{
/**
* #var Packages
*/
private $packages;
/**
* #var UrlHelper
*/
private $urlHelper;
/**
* #var ObjectNormalizer
*/
private $normalizer;
public function __construct(Packages $packages, UrlHelper $urlHelper, ObjectNormalizer $normalizer)
{
$this->packages = $packages;
$this->normalizer = $normalizer;
$this->urlHelper = $urlHelper;
}
public function normalize($user, $format = null, array $context = [])
{
/** #var array */
$data = $this->normalizer->normalize($user, $format, $context);
$avatar = null;
if ($user->getAvatarFilename()) {
$path = $this->packages->getUrl('uploads/avatars/'.$user->getAvatarFilename());
$avatar = $this->urlHelper->getAbsoluteUrl($path);
}
$data['avatar'] = $avatar;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof User;
}
}
The problem is that this property doesn't appear in the documentation as it's added by the custom normalizer. How can I add documentation for it (eg. type, example etc...)?
if this still relevant for you or somebody else:
You can add a custom field to the openapi model with this: https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification
You need to add $avatar field to your entity.
Like you suggest in the comments you could add a not mapped property to your entity and document it in the annotations, and yes it's hacky like they said here!... it is suggested in the SymfonyCast tutorials
Just remember the downside to this approach: our documentation has no
idea that this isMe field exists. If we refresh this page and open the
docs for fetching a single User... yep! There's no mention of isMe. Of
course, you could add a public function isMe() in User, put it in the
user:read group, always return false, then override the isMe key in
your normalizer with the real value. That would give you the custom
field and the docs. But sheesh... that's... getting kinda hacky.
I created a Entity with a custom contoller:
// api/src/Entity/UserRegistration.php
namespace App\Entity;
use ...
/**
* UserRegistraion Data
*
* #ApiResource(collectionOperations={},itemOperations={"post"={
* "method"="POST",
* "path"="/register",
* "controller"=CreateUser::class}})
*
*/
class UserRegistration
{
.....
/**
* #var string The E-mail
*
* #Assert\NotBlank
* #Assert\Email(
* message = "The email '{{ value }}' is not a valid email.",
* checkMX = true
* )
*/
public $email;
.....
And a custom Controller:
// api/src/Controller/CreateUser.php
class CreateUser
{
.....
public function __invoke(UserRegistration $data): UserRegistration
{
return $data;
}
}
When I call the controller with wrong data (e.g wrong email-address) I would expect an validation error, but it is not checked.
Is there a way to do this?
Api Platform does the validation on the result of your controller, to make sure your data persisters will receive the right information. Thus you may get invalid data when entering your controller, and need to perform the validation manually if your action needs a valid object.
The most common approaches are either using a Form, which provides among other things validation, or just the Validator as a standalone component. In your case you - since are using ApiPlatform - the latter would be the better choice as you don't need to render a form back to the user, but instead return an error response.
First you will need to inject the Validator into your Controller:
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class CreateUser
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function __invoke(UserRegistration $data): UserRegistration
{
$errors = $this->validator->validate($data);
if (count($errors) > 0) {
throw new ValidationException($errors);
}
return $data;
}
}
You can also check how ApiPlatform does it by looking at the ValidateListener. It provides some additional features, e.g. for validation groups, which you don't seem to need at this point, but might be interesting later. ApiPlatform will then use its ValidationExceptionListener to react on the Exception you throw and render it appropriately.
I'm trying to validate my entity via static callback.
I was able to make it work following the Symfony guide but something isn't clear to me.
public static function validate($object, ExecutionContextInterface $context, $payload)
{
// somehow you have an array of "fake names"
$fakeNames = array(/* ... */);
// check if the name is actually a fake name
if (in_array($object->getFirstName(), $fakeNames)) {
$context->buildViolation('This name sounds totally fake!')
->atPath('firstName')
->addViolation()
;
}
}
It works fine when I populate my $fakeNames array but what if I want to make it "dynamic"? Let's say I want to pick that array from the parameters or from the database or wherever.
How am I supposed to pass stuff (eg. the container or entityManager) to this class from the moment that the constructor doesn't work and it has to be necessarily static?
Of course my approach may be completely wrong but I'm just using the symfony example and few other similar issues found on the internet that I'm trying to adapt to my case.
You can create a Constraint and Validator and register it as service so you can inject entityManager or anything you need, you can read more here:
https://symfony.com/doc/2.8/validation/custom_constraint.html
or if you are on symfony 3.3 it is already a service and you can just typehint it in your constructor:
https://symfony.com/doc/current/validation/custom_constraint.html
This is the solution I was able to find in the end.
It works smoothly and I hope it may be useful for someone else.
I've set the constraint on my validation.yml
User\UserBundle\Entity\Group:
constraints:
- User\UserBundle\Validator\Constraints\Roles\RolesConstraint: ~
Here is my RolesConstraint class
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\Validator\Constraint;
class RolesConstraint extends Constraint
{
/** #var string $message */
public $message = 'The role "{{ role }}" is not recognised.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and here is my RolesConstraintValidator class
<?php
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RolesConstraintValidator extends ConstraintValidator
{
/** #var ContainerInterface */
private $containerInterface;
/**
* #param ContainerInterface $containerInterface
*/
public function __construct(ContainerInterface $containerInterface)
{
$this->containerInterface = $containerInterface;
}
/**
* #param \User\UserBundle\Entity\Group $object
* #param Constraint $constraint
*/
public function validate($object, Constraint $constraint)
{
if (!in_array($object->getRole(), $this->containerInterface->getParameter('roles'))) {
$this->context
->buildViolation($constraint->message)
->setParameter('{{ role }}', $object->getRole())
->addViolation();
}
}
}
Essentially, I set up a constraint which, every time a new user user is registered along with the role, that role must be among those set in the parameters. If not, it builds a violation.
I wish for the project need to override the class User in Silex 2.0:
Symfony\Component\Security\Core\User;
I absolutely do not see how.
My ultimate goal is to overload the method:
final class User implements AdvancedUserInterface
{
/**
* {#inheritdoc}
*/
public function getRoles()
{
return unserialize($this->roles[0]); // work (overload)
return $this->roles; // Not work (delete)
}
}
Have you any idea how to do this?
Thank you !
The final class User is only defined for the InMemoryUserProvider.
To read this database from Silex, you have to code your own UserProvider and configure it.
Instead of extending the User class, the correct value of $roles can be set in the constructor.
If you took the documentation example, you should replace this line:
return new User($user['username'], $user['password'], explode(',', $user['roles']), true, true, true, true);
By this:
return new User($user['username'], $user['password'], unserialize($user['roles']), true, true, true, true);
Otherwise, the best practice is to create your own User class.
/**
* User is the user implementation used by the in-memory user provider.
*
* This should not be used for anything else.
*/
I'm trying to run a console command in symfony2 in which some properties of a certain class are being updated. One of the properties has got a corresponding reviewedBy-property which is being set by the blameable-behaviour like so:
/**
* #var bool
* #ORM\Column(name="public_cmt", type="boolean", nullable=true)
*/
private $publicCmt;
/**
* #var User $publicCmtReviewedBy
*
* #Gedmo\Blameable(on="change", field="public_cmt")
* #ORM\ManyToOne(targetEntity="My\Bundle\EntityBundle\Entity\User")
* #ORM\JoinColumn(name="public_cmt_reviewed_by", referencedColumnName="id", nullable=true)
*/
private $publicCmtReviewedBy;
When i run the task there's no user which can be 'blamed' so I get the following exception:
[Doctrine\ORM\ORMInvalidArgumentException]
EntityManager#persist() expects parameter 1 to be an entity object, NULL given.
However I can also not disable blameable because it's not registered as a filter by the time i start the task and programmatically trying to set the user through:
// create the authentication token
$token = new UsernamePasswordToken(
$user,
null,
'main',
$user->getRoles());
// give it to the security context
$this->getService('security.context')->setToken($token);
doesn't work. Anyone got an idea?
If you use the StofDoctrineExtensionsBundle you can simply do :
$this->container->get('stof_doctrine_extensions.listener.blameable')
->setUserValue('task-user');
see : https://github.com/stof/StofDoctrineExtensionsBundle/issues/197
First of all, I'm not sure if 'field' cares if you use the database column or the property, but you might need to change it to field="publicCmt".
What you should do is override the Blameable Listener. I'm going to assume you are using the StofDoctrineExtensionsBundle. First override in your config:
# app/config/config.yml
stof_doctrine_extensions:
class:
blameable: MyBundle\BlameableListener
Now just extend the existing listener. You have a couple options - either you want to allow for NULL values (no blame), or, you want to have a default user. Say for example you want to just skip the persist and allow a null, you would override as such:
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
public function getUserValue($meta, $field)
{
try {
$user = parent::getUserValue($meta, $field);
}
catch (\Exception $e) {
$user = null;
return $user;
}
protected function updateField($object, $ea, $meta, $field)
{
if (!$user) {
return;
}
parent::updateField($object, $ea, $meta, $field);
}
}
So it tries to use the parent getUserValue() function first to grab the user, and if not it returns null. We must put in a try/catch because it throws an Exception if there is no current user. Now in our updateField() function, we simply don't do anything if there is no user.
Disclaimer - there may be parts of that updateField() function that you still need...I haven't tested this.
This is just an example. Another idea would be to have a default database user. You could put that in your config file with a particular username. Then instead of returning null if there is no user from the security token, you could instead grab the default user from the database and use that (naturally you'd have to inject the entity manager in the service as well).
Slight modification of the above answer with identical config.yml-entry: we can check if a user is set and if not: since we have access to the object-manager in the updateField-method, get a default-user, set it and then execute the parent-method.
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
protected function updateField($object, $ea, $meta, $field)
{
// If we don't have a user, we are in a task and set a default-user
if (null === $this->getUserValue($meta, $field)) {
/* #var $ur UserRepository */
$ur = $ea->getObjectManager()->getRepository('MyBundle:User');
$taskUser = $ur->findOneBy(array('name' => 'task-user'));
$this->setUserValue($taskUser);
}
parent::updateField($object, $ea, $meta, $field);
}
}