Symfony 4, edit user entity and save new password - php

I executed the make:crud command for generate some files for the entity user.
All works like a charm, but I have one problem when I edit a user.
When I edit a user, I can :
change the password and save
or
don't modify the password and save
From the generated user 'edit' controller:
/**
* #Route("/{id}/edit", name="user_edit", methods={"GET","POST"})
*/
public function edit(Request $request, User $user): Response
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// HERE : How can I check if the password was changed ?
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('user_index', [
'id' => $user->getId(),
]);
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
'title' => 'edit userr'
]);
}
How can I check if the password was changed ? The user variable contains the new password value...
If the password is new, I must use the encoder service. If not, I just must update the user with new data

For this it helps to have a class variable in your User that is not written to the database. In your form you can then use this variable to temporarily store the updated password in, that you then encode and remove. This is what the eraseCredentials()-method in the UserInterface is for.
For example in your User you could have
class User implements UserInterface
{
private $plainPassword;
// ...
public function setPlainPassword(string $plainPassword)
{
$this->plainPassword = $plainPassword;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function eraseCredentials()
{
$this->plainPassword = null;
}
}
Notice how private $plainPassword does not have any ORM-annotations, that means it will not be stored in the database. You can however use validation constraints, e.g. if you want to make sure that passwords have a minimum length or a certain complexity. You will still require the original password field that stores the encrypted password.
You then add this field to your user update-form instead of the actual password field. In your controller you can then only check if the new plainPassword-field was filled out and then read the value, encode it and replace the actual password field.
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
if ($user->getPlainPassword() !== null) {
$user->setPassword($this->userPasswordEncoder->encode(
$user->getPlainPassword(),
$user
);
}
// ...
Another way of doing this, without adding this "helper"-property to the user is using an unmapped form field:
# UserForm
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('plainPassword', PasswordType::class, ['mapped' => false])
;
}
The controller will look similar, only you fetch the data from the unmapped field instead of from the user:
$form->get('plainPassword')->getData();

Related

Why isPasswordValid() function always return false?

I am working on a simple reset password system with Symfony4.
I don't know why isPasswordValid() always return false?
I am using Bcrypt to Hash the password
Here is some code of Security.yaml :
security:
encoders:
App\Entity\User:
algorithm: bcrypt
I don't know why isPasswordValid() Always returns false.
I manually tried this:
$pass="000000000";
dump($encoder->isPasswordValid($user, $pass));
die();
and it dump false..
Here is the function I wrote on the controller:
/**
* #Route("/password", name="change_pass", methods={"GET","POST"})
* #IsGranted("ROLE_USER")
*/
public function edit(Request $request,UserPasswordEncoderInterface $encoder): Response
{
$user = $this->getUser();
$form = $this->createForm(ResetPassType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$oldPassword = $request->request->get('reset_pass')['oldPassword'];
$newPassword = $user->getPassword();
if ($encoder->isPasswordValid($user, $oldPassword)) {
$hash = $encoder->encodePassword($user,$newPassword);
$user->setPassword($hash);
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', 'Your password is succesfully changed');
}else {
$this->addFlash('fail', 'old password is wrong');
}
}
$this->getDoctrine()->getManager()->refresh($user);
return $this->render('consultant/changepass.html.twig', [
'form' => $form->createView(),
]);
}
Here is the form ResetPassType :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('oldPassword', PasswordType::class, [
'mapped' => false,])
->add('password',PasswordType::class)
;
}
Short answer: you are using the new password as the old password's hash.
Explanation:
I guess you bound your form to your user class.
The password field in your form is mapped, meaning it updates your user's password property.
Therefore, as the property should contain the hash of the "old password" but contains your new password, it can't validate.
Solution and improvements:
The quickest solution would be to "unmap" the password field or to remove the data_class form option, and to replace a line in your controller:
// before: $newPassword = $user->getPassword();
$newPassword = $form->get('password')->getData();
I'd suggest to:
unbind the form to your user class
use the UserPassword validation constraint for your oldPassword field
fetch the form data using $form->getData() instead of accessing the $request manually
You will change your form because if you keep the field password, the password of the user will be changed by form too, so the right way is :
-in your Form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('oldPassword', PasswordType::class, [
'mapped' => false,])
->add('newPassword',PasswordType::class, [
'mapped' => false,])
;
}
and then in your controller change the way you get the new password
$oldPassword = $request->request->get('reset_pass')['oldPassword'];
$newPassword = $request->request->get('reset_pass')['newPassword'];
Have fun :)

Laravel - Authorization to patch function

i have this PATCH function but i need to add some form of authorization to ensure you can only edit/update a film that is associated with the current user, can i get some help on how to add this
controller function:
public function update(string $id)
{
$this->user = Auth::user();
$this->film = film:findOrFail($id);
return $this->film->toJson();
}
I've looked at the laravel docs at the validation section and seen this example
$validatedData = $request->validate([
'title' => 'required|unque:posts|max:255',
'body' => 'required',
]);
i then added my own validation at the top of the file
protected $validation = [
'name' => 'string',
'description' => 'new description'
];
im a little lost on how i implement authorization to ensure only a current user can update a film?
What you're looking for is not a form validation, but a User Authorization (as in the comments). So you should have a look at the official documentation. In your case you should write a FilmPolicy that may look like to this (I will skip the registration part... It can be easily understood from the docs):
class FilmPolicy {
/**
* Determine if the given film can be updated by the user.
*
* #param \App\User $user
* #param \App\Post $post
* #return bool
*/
public function update(User $user, Film $film)
{
return $user->id === $film->user_id; // Or whatever is your foreign key
}
}
Then you should update your controller in order to handle the authorization as follow:
public function update(string $id)
{
$this->film = film::findOrFail($id);
$this->authorize('update', $this->film);
return $this->film->toJson();
}
Since this method simply throws an exception, you can have a more elaborate response as explained in the docs
Ok basically, to enable what you need in a simple way, what you can do is this;
First pass the 'user_id' to the controller.
public function update(string $id, $userid)
{
$user = Auth::user();
$id = $user->id;
if($id == $userid)
{
$this->user = Auth::user();
$this->film = film::findOrFail($id);
return $this->film->toJson();
}else{
return "Not Authorized";
}
}
If im not misunderstanding your question, this basically allows only the user who is logged in to update his film. if he goes into any other profile, the id's would mismatch and thus return a not authorized prompt.

Symfony 3, populating token and refreshing user

repository with issue
I have a form for entity User with email field:
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(),
new Email([
'checkMX' => true,
])
],
'required' => true
])
when i'm editing email to something like test#gmail.com1 and submit form it shows me error "This value is not a valid email address." THat's ok, but after that symfony populate wrong email into token and when i'm going to any other page or just reload page, i'm getting this:
WARNING security Username could not be found in the selected user
provider.
i think question is: why symfony populate wrong Email that failed validation into token and how i could prevent it?
controller:
public function meSettingsAction(Request $request)
{
$user = $this->getUser();
$userUnSubscribed = $this->getDoctrine()->getRepository('AppBundle:UserUnsubs')->findOneBy(
[
'email' => $user->getEmail(),
]
);
$form = $this->createForm(UserSettingsType::class, $user);
$form->get('subscribed')->setData(!(bool)$userUnSubscribed);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/**
* #var $user User
*/
$user = $form->getData();
/** #var UploadedFile $avatar */
$avatar = $request->files->get('user_settings')['photo'];
$em = $this->getDoctrine()->getManager();
if ($avatar) {
$avatar_content = file_get_contents($avatar->getRealPath());
$avatarName = uniqid().'.jpg';
$oldAvatar = $user->getPhoto();
$user
->setState(User::PHOTO_STATE_UNCHECKED)
->setPhoto($avatarName);
$gearmanClient = $this->get('gearman.client');
$gearmanClient->doBackgroundDependsOnEnv(
'avatar_content_upload',
serialize(['content' => $avatar_content, 'avatarName' => $avatarName, 'oldAvatar' => $oldAvatar])
);
}
$subscribed = $form->get('subscribed')->getData();
if ((bool)$userUnSubscribed && $subscribed) {
$em->remove($userUnSubscribed);
} elseif (!(bool)$userUnSubscribed && !$subscribed) {
$userUnSubscribed = new UserUnsubs();
$userUnSubscribed->setEmail($form->get('email')->getData())->setTs(time());
$em->persist($userUnSubscribed);
}
$user->setLastTs(time());
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
$this->get('user.manager')->refresh($user);
return $this->redirectToRoute('me');
}
return $this->render(
':user:settings.html.twig',
[
'form' => $form->createView(),
]
);
}
UPD:
it works fine if i change in OAuthProvider:
/**
* #param \Symfony\Component\Security\Core\User\UserInterface $user
*
* #return \Symfony\Component\Security\Core\User\UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getName());
}
to:
/**
* #param \Symfony\Component\Security\Core\User\UserInterface $user
*
* #return \Symfony\Component\Security\Core\User\UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $this->userManager($user->getId());
}
but it seems to be dirty hack.
Thanks.
Your user token seems to be updated by the form, even if the email constraint stop the flush.
Can you check if your form past the isValid function ?
You can maybe try to avoid it with an event listener or a validator.
With an event SUBMIT you should be able to check the email integrity, and then add a FormError to avoid the refreshUser.
This is a tricky one, thanks to the repository it was easier to isolate the problem. You are binding the user object form the authentication token to the createForm() method. After the
$form->handleRequest($request)
call the email off the token user object is updated.
I first thought to solve this by implementing the EquatableInterface.html in the User entity but this did not work, as the compared object already had the wrong email address set.
It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user. This interface requires an isEqualTo() method.)
Than I thought about forcing a reload of the user from the db and resetting the security token, but in the it came to my mind, that it might be sufficient to just refresh the current user object from the database in case the form fails:
$this->get('doctrine')->getManager()->refresh($this->getUser());`
In your controller, this would solve your issue.
/**
* #Route("/edit_me", name="edit")
* #Security("has_role('ROLE_USER')")
*/
public function editMyselfAction(Request $request) {
$form = $this->createForm(User::class, $this->getUser());
if ($request->isMethod(Request::METHOD_POST)) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
} else {
$this->get('doctrine')->getManager()->refresh($this->getUser());
}
}
return $this->render(':security:edit.html.twig',['form' => $form->createView()]);
}
Alternative solution
The issue at the Symfony repository resulted in some valuable input about Avoiding Entities in Forms and
Decoupling Your Security User which provides a more complex approach for a solution to your problem.

Hash password before saving with Laravel Backpacker

A simple question: how do I modify (hash) the request value before saving it with Laravel Backpacker CRUD admin?
As far as i understand, it should be done somewhere before these methods are executed in the crud controller:
public function store(StoreRequest $request)
{
return parent::storeCrud();
}
public function update(UpdateRequest $request)
{
return parent::updateCrud();
}
but I have no idea how to do it correctly.
Edit: the request is not a Request object, but rather StoreRequest or UpdateRequest that looks something like this:
Fix:
public function update(UpdateRequest $request)
{
// Hash password before save
if (!empty($request->password)) {
$request->offsetSet('password', Hash::make($request->password));
}
return parent::updateCrud($request); // <-- Pass the modified request, otherwise the CRUD reads it again from post data
}
You can update $request values using the offsetSet method
$request->offsetSet('name', $newName);
Edit: To update user password you can do something like this:
public function update_password(Request $request)
{
$user = User::find(Auth::user()->id);
if (Hash::check($request->old_password, $user->password)) {
$user->fill([
'password' => Hash::make($request->password)
])->update();
return redirect()->back()->with('message' => 'Your password has been updated.');
}
else {
return redirect()->back()->with('message' => 'The password entered do not match our records.');
}
}
I did not check the code but it should work. Now update it to your needs.
If you're asking about how to modify data in $request variable, you can just do this:
$request->property = 'New value';
Also, you can add data to reuqest itself (not into variable):
request()->request->add(['key' => 'value']);

How can I update mapped fields only when a condition is satisfied?

In my Symfony2 application I created a custom form to edit objects of a User class. Users have a password property which contains a hash of the user's password. For obvious reasons, I do not want to echo this property's value into a field. However, I want a password field on my form so that when editing a user it is possible to change the user's password.
This password field should behave as follows:
If the user has a password set, then the field should contain ********.
If the user has no password set, then the field should be empty.
(It turns out the previous two points are impossible to achieve with my current architecture, so instead I am going for:) When the page is loaded, the field should be empty, regardless of whether the user has a password set.
If the field is posted with content, then the user's password should be set to the hashed value of the field.
If the field is posted empty, the user's password should not be changed and, more importantly, not cleared.
I thought of implementing this with a custom data transformer. However, the data transformer does not provide me with a way to skip updating the user's password property when the password field is posted empty.
Where do I need to extend the framework to add custom logic deciding which fields should be updated?
UPDATE
This is the legacy code I am trying to replace:
/* SomeController.php */
$pass = $request->get('password');
if (strlen($pass) >= 5 && strlen($pass) <= 16) {
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($pass, $user->getSalt());
$user->setPassword($password);
}
I can live with removing the string length checks. All I want to check is whether something has been entered.
As you can see, I can not simply move this code to a data transformer as it needs to access the $user which is the user we are currently editing. I don't think it is a good idea to create a service providing this value.
Just insert a control directly into your entity method and use data transformer (as you have insight)
So your entity will be
class User
{
//some properties and methods here
public function setPassword($pwd = null) {
if (null !== $pwd) {
$this->password = //do actions here like hash or whatever
}
//don't update the password
}
}
If you want to take advantage only of DataTransformers, you could still do what you need that way
use Symfony\Component\DependencyInjection\ContainerInterface;
class PasswordTransformer implements DataTransformerInterface
{
private $ci;
public function __construct(ContainerInterface $ci) {
$this->ci = $ci;
}
//...
public function reverseTransform($form_password) {
if (!$form_password) {
//return password already stored
return $this->ci->get('security.context')
->getToken()
->getUser()
->getPassword();
}
}
}
Of course you need to inject service_container service into you data transformer (or better, you should inject it into your form type's selector and pass to DataTransformer constructor as follows:
services:
your.bundle.password_selector_type:
class: Your\Bundle\Form\Type\PasswordSelectorType
arguments: ["#security.context"]
tags:
- { name: form.type, alias: password_selector_type }
For the form part, you should take a look a this widget.
http://symfony.com/doc/current/reference/forms/types/repeated.html
It provides an easy way to ask and treat confirmation on a field (it also hide values with stars when you set type to password).
$builder->add('password', 'repeated', array(
'type' => 'password',
'invalid_message' => 'The password fields must match.',
'options' => array('attr' => array('class' => 'password-field')),
'required' => true,
'first_options' => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password')));
It will check first and second options to be equal. If that's the case then your form will be considered as valid. Otherwise, invalid_message will be displayed and first field will set to the content typed by user while confirmation field (second option) will be emptied.
You can add some logic afterwards like hashing the password to finally persist your entity. (Extracting it in a form handler would be a good practice).
Here is what I came up with for now but I am not happy with the solution as it involves custom form processing in the controller. However, so far this is the only way I have found to make it work.
My form class adds an unmapped field for the user's password:
class UserType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('username')
->add('displayName')
->add('password', 'password', ['mapped' => false]);
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'MyCompany\MyBundle\Entity\User'));
}
public function getName() {
return 'user';
}
}
This field is then processed manually in my controller class:
class UserAdminController extends Controller {
public function editUserAction($userId, Request $request) {
$user = $this->getDoctrine()->getRepository('MyCompanyMyBundle:User')->findOneById($userId);
$form = $this->createForm('user', $user);
$form->handleRequest($request);
if ($form->isValid()) {
$newPassword = $form['password']->getData();
if ($newPassword !== "") {
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($newPassword, $user->getSalt());
$user->setPassword($password);
}
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
}
return $this->render(
"MyCompanyMyBundle:Admin/Management/User:Edit.html.php",
[
"form" => $form->createView()
]
);
}
}

Categories