Symfony - Edit User Entity without changing password - symfony

I'm running Symfony 3.4.14 and I developed my own User Bundle, I have a very bad experience with FOS then I don't want to use anymore. My goal is to let the admin users create/edit/remove users, I mean other users account.
I made :
the User entity
the Login form
the Registration form
.. and I'm stuck with the Update form. I want to let the admins edit a user without editing the password, but to give them the opportunity to do it if needed. Below is my EditUserAction in controller :
<?php
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function EditUserAction($id, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$user = $this->getDoctrine()->getRepository('CoreBundle:User')->findOneBy([ 'id'=>$id, 'deleted' => 0 ]);
if ( $user )
{
$old_password = $user->getPassword();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
// If admin changed the user password
if ( $user->getPlainPassword() )
{
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
}
// If admin didn't change the user password, we persist the old one
else
{
$user->setPassword($old_password);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
}
return array('form' => $form->createView());
}
return $this->redirectToRoute('admin_users');
}
Case 1 (when admin choose to change the user password) works well, but the other case (when admin don't want to change the password) fails. They are no way to let the 2 password inputs empty. I can't get rid of this validation error in the debug toolbar :
Path: data.plainPassword
Message: This value should not be blank
In order to avoid this error, as you can see in my controller above, I try to keep the old one (may not be a best practice, I know).

In your form / entity (where you defined validations) you should write either your own constraint (documentation) or validation callback (documentation). In there you can check - If value is null, don't validate, if not null run your validations.

You need to have two separate methods for both.
public function changePasswordAction(Request $request)
{
// your code for changing password.
}
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function editUserAction(User $user = null, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
//you can directly give your User Entity reference in parameters and you dont need to write an extra query to find user.
if ( $user === null){
//return user not found
}
else if($user->isDeleted() === false)
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
//your code
}
// your code
}
return $this->redirectToRoute('admin_users');
}

Related

Symfony, how to add a field to the database that isn't on the form

I want to use a form in Symfony to allow a user to add admin clients. The User class contains fields $username (string), $password (string) and $roles (array of user roles).
I want to be able to enter the username and password in a form and then add this new user to the database but I don't want to have to enter the role on the form, I want to code this in to always be ROLE_ADMIN.
In my controller, I have the following code:
class SysAdminController extends AbstractController
{
/**
* #Route("/sysAdmin", name="app_sysAdmin")
*/
public function sysAdmin(Request $request)
{
$user = new User();
$form = $this->createForm(AddAdminUserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$user->setRoles(['ROLE_ADMIN']);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
return $this->redirectToRoute('db_write_success_message');
}
return $this->render('/sysAdmin.html.twig', [
'form' => $form->createView()
]);
}
}
However, when I try to submit the form it gives the message "This value should not be blank." It appears to be related to the $roles field and is falling over on the $form->isValid() check. This field should indeed not be blank on the database, therefore in the User class is set to Notblank. However, I would like it to be blank on the form and then I'd like to populate this before adding it to the database.
In your entity User
Do u do this ?
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}

How to modify user Information after requiring his password with Symfony 4

I would like to create a page that allows the user to modify his personal infos.
I want him to enter his current password to modify any information.
I created a form based on the connected user when the form is submitted and valid, I want to check if the password is valid using the function isPasswordValid() of my passwordEncoder..
My problem is that when this function is called with $user as parameter, it always returns false. I found where this problem comes from, it's because the $user used as parameter as been modified when the form has been submitted. I've tried declaring another variable to stock my Initial User ($dbUser for example) and using another to instance the form but when I dump the
$dbUser it has been modified and I don't know why...
The variable shouldn't be changed after the submit because it's never used... I can't find what I doing wrong...
/**
* #Route("/mes-infos", name="account_infos")
*/
public function showMyInfos(Request $request, UserRepository $userRepo)
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
// $dbUser = $userRepo->findOneBy(['id' => 13]);
$form = $this->createForm(UserModificationType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$enteredPassword = $request->request->get('user_modification')['plainPassword'];
$passwordEncoder = $this->passwordEncoder;
$manager = $this->getDoctrine()->getManager();
if ($passwordEncoder->isPasswordValid($user, $enteredPassword)) {
dd('it works!!!!!');
// $manager->persist($user);
// $manager->flush();
} else {
dd('It\'s not!!!!');
}
}
return $this->render('account/myaccount-infos.html.twig', [
'form' => $form->createView(),
]);
}
The better solution is to use a constraint.
Symfony already implements a UserPasswordConstraint.
You can add it in your entity directly. Be careful to declare a group for this constraint. If you don't do it, your use case "user update" will works fine, but the use case "user creation" will now failed.
namespace App\Entity;
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
class User
{
//...
/**
* #SecurityAssert\UserPassword(
* message = "Wrong value for your current password",
* groups = {"update"}
* )
*/
protected $password;
//...
}
In your form, specified the validation groups by updating (or adding) the configureOptions method:
//App\Form\UserModificationType
//...
class UserModificationType {
//...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ...
'validation_groups' => ['update'],
]);
}
}
Then your $form->isValid() will automatically test the password. So, you can removed all lines in the "if condition".
/**
* #Route("/mes-infos", name="account_infos")
*/
public function showMyInfos(Request $request, UserRepository $userRepo)
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
$form = $this->createForm(UserModificationType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd('Password is good! it works!!!!!');
}
return $this->render('account/myaccount-infos.html.twig', [
'form' => $form->createView()
]);
}
But I think it is a better practice to use a Model like in the documentation when you update your user entity.

passwordEncoder generated hash for non-authentication purpose fails on verification

For a user to register with my system, I'm sending a confirmation mail with a generated link. On click the system should set an active property to true.
To create the confirm link, I generate a md5 hashed random_byte value. This value is part of the link I send to the user. The value is hashed with the passwordEncoder and stored in my database.
When the user clicks the link, the system checks the value against the hash in my database and should return true, to fully activate the user. For some reason the verification fails.
I checked, that the generated $identifier value in register() is the one that gets send to the user. They match. So no mistake there.
At first I wanted to use password_verify() only, but that didn't work out either.
I simply have no clue, why it's not working. My last guess is, that there is some Symfony dark magic going on, that I'm not aware of.
public function register(EntityManagerInterface $entityManager, Request $request, UserPasswordEncoderInterface $passwordEncoder, MailerInterface $mailer)
{
// Register-Form
$form = $this->createForm(RegisterFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var User $user */
$user = $form->getData();
$user->setPassword($passwordEncoder->encodePassword(
$user,
$form['plainPassword']->getData()
));
// create activation hash
$identifier = md5(random_bytes(16));
$user->setActiveHash(
$passwordEncoder->encodePassword(
$user,
$identifier
)
);
// send activation mail with activation hash
$message = (new TemplatedEmail())
->subject('...')
->from('...')
->to($user->getEmail())
->textTemplate('emails/registration.txt.twig')
->context([
'mail' => $user->getEmail(),
'identifier' => $identifier,
])
;
$mailer->send($message);
// persist & flush
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', '...');
//return $this->redirectToRoute('login');
return $this->render('home/index.html.twig');
}
return $this->render('security/register.html.twig', [
'registerForm' => $form->createView()
]);
}
/**
* #Route("/confirm/{email}/{identifier}", name="register.confirm")
*/
public function confirm($email, $identifier, EntityManagerInterface $entityManager, UserPasswordEncoderInterface $passwordEncoder)
{
$user = $entityManager->getRepository(User::class)->findOneBy([
'email' => $email,
'active' => false
]);
if (!$user) {
// fail no user with email
die('no such user');
}
if( !$passwordEncoder->isPasswordValid($user, $identifier) ){
// Hash verification failed
die('hash failed');
}
$user->setActive(true);
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', '...');
return $this->redirectToRoute('login');
}
EDIT:
I tried manually hashing a string with password_hash('a_string', PASSWORD_ARGON2ID) and it throws an exception that PASSWORD_ARGON2ID is an undefined constant. PASSWORD_ARGON2I works. And Symfony somehow uses Argon2ID as my password strings and the activeHash strings generated through encodePassword() start with "$argon2id..." How is that possible?
Ok. So I was running PHP 7.2 via MAMP locally. As (before it was deleted) mentioned in the comments by msg, Argon2ID hashing was probably provided by Sodium. 7.2 password_verify() on the other hand was not able to verify a argon2id hash.
Upgraded to PHP 7.3.7 and no problem anymore.

Symfony, check if the user has accepted new T&C

I need to check if the user has accepted the latest privacy policy, before executing any controller. Something like this:
if($user->getAcceptedTnc() < 2) // unless I'm in some specific routes...
{
return $this->render('app/privacyPolicy.html.twig');
// or alternatively do AppController::privacyPolicyAction()
}
Where can this be done?
I've thought at logging out all the users and putting this in some authentication listener...
I solved with a onKernelController listener, so I'm doing the check at every page load:
public function onKernelController(FilterControllerEvent $event)
{
// [ return on some conditions (ajax calls, specific controller/routes, ...) ]
/** #var User $user */
$user = $this->token ? $this->token->getUser() : null;
if($user && $user !== 'anon.' && $user->getLastAcceptedTerms() < Utils::CURRENT_TERMS_VERSION) {
// [ return if the user is non-EU, has specific roles, etc...]
// The user must accept the new Terms. Show him AppController:privacyPolicyAction
$request = new Request();
$request->attributes->set('_controller', 'App\Controller\AppController:privacyPolicyAction');
$event->setController($this->controllerResolver->getController($request));
}
}

Manual authentication check Symfony 2

I'm working on a Symfony 2 application where the user must select a profile during the login process.
Users may have multiples profiles to work with and they only know their own profiles. So first, I need to prompt for username and password, if those are correct, I should not login the user, I need to prompt for profile witch user will use during the session.
So, I show a form with a username and password field, and send it using an Ajax request, that request responds with the profile list if username and password are correct or an error code otherwise. Finally the user logs into the system using username, password and profile.
The problem is that I don't know how to check if authentication data is correct (using all my authentication managers, users providers, etc) to accomplish this intermediate step (prompts for profile) without in fact logging the user.
Can anyone help me with this?
A problem with #Jordon's code is that it will not work with hashing algorithms that generate different hashes for the same password (such as bcrypt that stories internally its parameters, both the number of iterations and the salt). It is more correct to use isPasswordValid of the Encoder for comparing passwords.
Here is the improved code that works fine with bcrypt:
$username = trim($this->getRequest()->query->get('username'));
$password = trim($this->getRequest()->query->get('password'));
$em = $this->get('doctrine')->getManager();
$query = $em->createQuery("SELECT u FROM \Some\Bundle\Entity\User u WHERE u.username = :username");
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
if ($user) {
// Get the encoder for the users password
$encoder_service = $this->get('security.encoder_factory');
$encoder = $encoder_service->getEncoder($user);
// Note the difference
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
// Get profile list
} else {
// Password bad
}
} else {
// Username bad
}
You could do something like this to retrieve the user and manually test the password -
$username = trim($this->getRequest()->query->get('username'));
$password = trim($this->getRequest()->query->get('password'));
$em = $this->get('doctrine')->getEntityManager();
$query = $em->createQuery("SELECT u FROM \Some\Bundle\Entity\User u WHERE u.username = :username");
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
if ($user) {
// Get the encoder for the users password
$encoder_service = $this->get('security.encoder_factory');
$encoder = $encoder_service->getEncoder($user);
$encoded_pass = $encoder->encodePassword($password, $user->getSalt());
if ($user->getPassword() == $encoded_pass) {
// Get profile list
} else {
// Password bad
}
} else {
// Username bad
}
Once you've got your profile back from the client, you can perform the login manually in the AJAX server controller easily enough too -
// Get the security firewall name, login
$providerKey = $this->container->getParameter('fos_user.firewall_name');
$token = new UsernamePasswordToken($user, $password, $providerKey, $user->getRoles());
$this->get("security.context")->setToken($token);
// Fire the login event
$event = new InteractiveLoginEvent($this->getRequest(), $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
Might need a few use lines -
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
I used the code from #Jordon and #Potor Polak to wrap the logic in a standalone service that used the current access token to validate the password. Maybe some needs this:
services.yml:
app.validator.manual_password:
class: AppBundle\Service\ManualPasswordValidator
arguments:
- '#security.token_storage'
- '#security.encoder_factory'
ManualPasswordValidator.php:
<?php
namespace AppBundle\Service;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
/**
* Class ManualPasswordValidator
*
* #package AppBundle\Service
*/
class ManualPasswordValidator
{
/**
* #var EncoderFactory
*/
protected $encoderFactory;
/**
* #var TokenStorage
*/
protected $tokenStorage;
/**
* ManualPasswordValidator constructor.
*
* #param EncoderFactory $encoderFactory
* #param TokenStorage $tokenStorage
*/
public function __construct(TokenStorage $tokenStorage, EncoderFactory $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
$this->tokenStorage = $tokenStorage;
}
/**
* #param $password
* #return bool
*/
public function passwordIsValidForCurrentUser($password)
{
$token = $this->tokenStorage->getToken();
if ($token) {
$user = $token->getUser();
if ($user) {
$encoder = $this->encoderFactory->getEncoder($user);
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return true;
}
}
}
return false;
}
}
After this you can inject the ManualPasswordValidator wherever you want and use it like:
$password = $request->get('password');
$passwordIsValid = $this->manualPasswordValidator->passwordIsValidForCurrentUser($password);
The only way I could authenticate my users on a controller is by making a subrequest and then redirecting. Here is my code, I'm using silex but you can easily adapt it to symfony2:
$subRequest = Request::create($app['url_generator']->generate('login_check'), 'POST', array('_username' => $email, '_password' => $password, $request->cookies->all(), array(), $request->server->all());
$response = $app->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, false);
return $app->redirect($app['url_generator']->generate('curriculos.editar'));
In Symfony 4, the usage of the UserPasswordEncoderInterface is recommended in Controllers. Simply add a UserPasswordEncoderInterface as a parameter to the function in which you want to check the password and then add the code below.
public function changePasswordAction($old, $new, UserPasswordEncoderInterface $enc) {
// Fetch logged in user object, can also be done differently.
$auth_checker = $this->get('security.authorization_checker');
$token = $this->get('security.token_storage')->getToken();
$user = $token->getUser();
// Check for valid password
$valid = $encoder->isPasswordValid($user, $old);
// Do something, e.g. change the Password
if($valid)
$user->setPassword($encoder->encodePassword($user, $new));
}
Symfony 5.4
Password validation can be done using UserPasswordHasherInterface
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AuthenticaitonServices
{
public function __construct(UserPasswordHasherInterface $hasher)
{
$this->hasher = $hasher;
}
public function validate($request)
{
$form = [
"username" => $request->request->get("_username"),
"password" => $request->request->get("_password")
];
if(!$this->hasher->isPasswordValid($user, $form['password']))
{
// Incorrect Password
} else {
// Correct Password
}
isPasswordValid returns a bool response
If anyone checking solution for password validation in Symfony 5.4.
Above code is for validating password posted from a login form.
Hope this is helpful.

Resources