Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Validate current password when updating user

default discord avatar
hellboy124124last year
6

Hey,


I'm currently trying to implement the typical settings page, where you allow the user to change the password. Now, I'm having an issue, because I have a beforeValidateHook function on the Users collection - in there I do something like this:


export const usersBeforeValidateHook: CollectionBeforeValidateHook<User> = async ({
  data,
  req: { user, payload },
  originalDoc,
}) => {
  if (user && originalDoc && !user.roles.includes('admin') && !user.roles.includes('moderator')) {
    throw new Error('Only an admin can edit another admin');
  }

  if (user && user.id === originalDoc.id) {
    const newData = data as User & {
      currentPassword?: string;
      newPassword?: string;
    };

    if (typeof newData.newPassword !== 'undefined') {
      const providedCurrentPasswordHash = payload.encrypt(newData.currentPassword ?? '');

      if (user.password !== providedCurrentPasswordHash) {
        throw new Error('The provided password does not match the current password.');
      }

      if (newData.newPassword.length < 8) {
        throw new Error('Your password must be longer than 8 characters.');
      }

      data.password = payload.decrypt(newData.newPassword);

      delete (data as any).currentPassword;
      delete (data as any).newPassword;
    }
  }

  return data;
};


Now, as you can see - the issue is, that the payload.encrypt() function does not work, because I'm sure that this is just a generic encrypt function and not the same one that is used when registering or logging the user in, as that one would need to also need to take in the salt. I have gone through the payload API, but can not really find a way on how to implement that properly.


So I'm wondering how would I implement this functionality of changing the password in payload?

  • discord user avatar
    jesschow
    last year

    Hi @hellboy124124! A couple questions:



    1. Is this a settings page on your front end? Or a custom page in your Payload project?




    2. Is the purpose of this to throw a more specific error? Are you also using the access property to set permissions on your users collection?


      if (user && originalDoc && !user.roles.includes('admin') && !user.roles.includes('moderator')) {
        throw new Error('Only an admin can edit another admin');
      }


    To simplify this and avoid the need for encrypting/decrypting, I would add to the function above

    || user.id !== originalDoc.id

    , then remove the rest and take care of it on the front end.



    Now on the front end, you'd make an onSubmit function to take their old password and send a login request to the REST API. If the login is successful, make another request to update their password.



    You can take a look at how we do this for the Cloud settings page here:

    https://github.com/payloadcms/website/blob/main/src/app/cloud/settings/client_page.tsx
  • default discord avatar
    hellboy124124last year

    Thank you.



    1. Yes, this is on my frontend, which is actually a nextjs project - I have a /settings endpoint there, which basically just proxies the call to payload (PATCH /api/users/{id})


    2. This is not really an issue, this all works as expected.



    It's the bit after that, which does not work. At this point, the user is actually logged in, but from a security standpoint I think it's better for the user, when they are changing their password, to also provide the current password. At least that is the practice for the majority of platforms nowadays I think. So would there be any way to validate the provided current password somehow on the payload side - specifically on the User collection's beforeValidate hook?

  • discord user avatar
    jesschow
    last year

    You should also be able to do this with the

    beforeValidate

    hook , there is some useful information from Dan about how to use encrypt / decrypt functions from Payload here

    https://github.com/payloadcms/payload/discussions/1435#discussioncomment-4173265
  • default discord avatar
    hellboy124124last year

    Sadly that is not the same thing. Like mentioned in the original message - those do not work as those are general encryption functions, not the one that are used for the authentication process, which is the whole issue.

  • discord user avatar
    jesschow
    last year

    After reading your beforeValidate hook again, isn't

    user.password

    always returning as

    undefined

    ?



    At this point, the user is actually logged in, but from a security standpoint I think it's better for the user, when they are changing their password, to also provide the current password.

    You can still have them provide the current password again, then make a payload.login request within your beforeValidate hook and continue that way?

  • default discord avatar
    hellboy124124last year

    Ok, I did try now with the method suggested, but it's rather really hack-y:


    export const usersBeforeValidateHook: CollectionBeforeValidateHook<User> = async ({
      data,
      req: { user, payload },
      originalDoc,
    }) => {
      if (user && originalDoc && !user.roles.includes('admin') && !user.roles.includes('moderator')) {
        throw new Error('Only an admin can edit another admin');
      }
    
      if (user && user.id === originalDoc.id) {
        const newData = data as User & {
          currentPassword?: string;
          newPassword?: string;
        };
    
        if (
          typeof newData.currentPassword !== 'undefined' &&
          typeof newData.newPassword === 'undefined'
        ) {
          throw new Error('Please provide the new password');
        } else if (typeof newData.newPassword !== 'undefined') {
          try {
            await payload.unlock({
              collection: 'users',
              data: {
                email: user.email,
              },
              overrideAccess: true,
            });
    
            await payload.login({
              collection: 'users',
              data: {
                email: user.email,
                password: newData.currentPassword,
              },
              overrideAccess: true, // Not really working, as it will still block the user, without the .unlock() call above
            });
          } catch (err) {
            throw new Error('The current password is incorrect');
          }
    
          if (newData.newPassword.length < 8) {
            throw new Error('Your password must be longer than 8 characters.');
          }
    
          data.password = newData.newPassword; // TODO: This part is not working
    
          delete (data as any).currentPassword;
          delete (data as any).newPassword;
        }
      }
    
      return data;
    };


    Still not full working, because when I provide the new password in "data.password = newData.password", it is not saving the new password. Is there any reason for that? And yes, I can 100% confirm that if I console.log() right before the return data;, I do get the correct (raw) password there.

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get dedicated engineering support directly from the Payload team.