forgotPassword should optionally reset header

I’m using forgotPassword as the easiest, most straightforward way to send an email to my new users when signing up, because there’s been a modification recently about how we store the tokens in the db. The raw version of a token is only displayed once and its hashed version is stored in the db, so if I want to pass the raw version of the token in my email template - I have to do it using forgotPassword.

The issue with this is that it effectively kills my session, thus preventing my new user to get logged in right at sign up: https://github.com/redwoodjs/redwood/blob/main/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts#L539

      return [
        response ? JSON.stringify(responseObj) : '',
        {
          ...this._deleteSessionHeader,
        },
      ]

Should I use a different workflow to achieve this? Or would it be acceptable to make an option for api/src/functions/auth.ts>handler>forgotPasswordOptions?

What kind of email are you trying to send? A generic “Welcome” email, or something specific about the password?

I’m sending a welcome email with a link to invite the user to set their password - which they are not doing in the app yet.

I’d prefer to send emails like that out-of-band. I’ve recently been playing with both Inngest and Trigger.dev. They’d both be great for this kind of thing.
But that doesn’t solve your token issue.

Why can’t you just have the users pick their password when signing up?

1 Like

I’m trying a sign up process i have implemented for a client, minimalist on the credentials because it’s packed with a necessary onboarding process.

Once the onboarding is done the user can update their password through UI - or use the forgotten password at any time.

Why do you prefer to handle this out of band?

Because I can just fire off an event and then not have to care about it. I know it’ll get done eventually. I find my code gets a bit cleaner that way.

Unfortunately I can’t think of a great way to solve your usecase right now though :frowning:

Me neither!

I’m considering switching Auth provider but that would add extra days of work i find hard to accept - the change is breaking and the changelog didn’t really highlight it.

The easiest way is to let users set their own password, but this cannot be a permanent situation.

Could this get a bit more discussion within the CT? I am curious as to what would be a broader opinion on this limitation.

@noire.munich do you know which major broke the way you were doing this? And just to make sure I understand what you’d like, it sounds like you want an option to disable forgotPassword logging users out of their session?

@dom I would think it was 5.0.0, but I’d need to investigate way further to confirm. I haven’t pass through 6 yet and 4 didn’t have the issue as far as I remember.

That would help! If it defaults to logging out, current apps wouldn’t have to update and those looking for this would be able to have their needs met.

Hmm, I didn’t envision the Forgot Password flow to be used to set a password! I can’t see a way to do what you need the way the code currently is (forcibly logging the user out). Logging someone out is more of a security measure and I don’t know that skirting that is a good thing to do, but maybe I’m wrong and it’s not that big of a deal?

If you’re okay with doing a little more work, could I suggest a flow that’ll accomplish what you need without changes to Redwood itself?

  1. Add a field to the User table, something like verified and set to false by default.
  2. When a user signs up, you set a default password, even if it’s just a random string, and keep verified false
  3. After the user is created, send an email with a link to a new page you’re going to create, that will let them set their password
  4. In a layout, or some place that “wraps” the rest of your logged in app pages, have a check that if currentUser.verified === false then redirect to a “We sent you an email to set your password” page (and maybe a “Click here to resend” link). Note that the user is still logged in and their session is good, they just can’t go anywhere useful (yet)
  5. The user follows the link in the email, sending them to the new page to set their password
  6. On submit, use this code on the server side to hash the password the same way that dbAuth does internally, save it to the DB and set verified to true (I think you can also import the hashPassword function from @redwood/auth-dbauth-api)
  7. Do a reauthenticate() after submitting their new password and verified is now true, so you can redirect them anywhere you’d like and the app will work as normal!

Personally, doing that feels more “right” to me, and keeps the concerns around forgetting your password consistent, not mixed with setting the password in the first place.

3 Likes

One thing I realized yesterday when I discussed this with Dom is that this behavior lets any user log out any other user as long as they know (or can guess) their email address, right? Just have to type “john.smith@gmail.com” into the forgot password box and whatever user is connected to that email will be logged out. Could be pretty annoying…

Remember that with dbAuth “logging out” just means deleting the session cookie. So putting in someone else’s email doesn’t do anything to that other person: only the browser that’s actually accessing the Forgot Password form would have their cookie removed!

1 Like

This reply might be irrelevant since the post is a month old by now. But I also had to build a custom onboarding flow for my app. Regular registration was not feasible. Here is how I solved it.

  1. Wrote a rw script that imports user data from external system.
  2. Create a User record with an encrypted and hashed random password. I copy pasted the implementation used by the password reset package in the framework for that.
  3. Emulate the password reset mechanism by storing a new token (with a long duration)
  4. Sent off a “welcome to the app” email with the token in the email cta link
  5. Built a custom password page that said “create a password” instead of “reset your password”.

I liked this approach because this way I could reuse all the web helpers that are used in the regular password reset pages

2 Likes

I have an app where the users are invite-only - I want to disable the signup flow entirely. I think ideally the code that generate a token and call the forgotPassword callback could be separated out into its own method, allowing me to call it via the backend as either part of the createUser endpoint, or a new endpoint (obviously gated by RBAC). This is the code I’m talking about: https://github.com/redwoodjs/redwood/blob/2bfadd2ca4b2020e15226711520f04993ffeebff/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts#L510

I guess the other way I could do it would be to enable the signup flow, but put a hook in there somewhere to throw an error if the username isn’t already in the User table - kind of the opposite of what would normally be done on a signup flow.
It seems like an invite-only app isn’t that uncommon of a use case, so it would be nice if there was a well-paved path to support that.

1 Like

I’m over late on this one, sorry guys…
@rob your link might have aged a bit, can you confirm we’re talking about copying this code? The function hashPassword.

I’ll give it a try, recently I discovered resetPassword wasn’t working at all for me >< resetToken operations are ok but the hashedPassword and salt fields never change.
Will post an issue if that proves to be a relevant one.

Yep that’s the function. hashedPassword won’t change until the person actually follows the link and changes their password (the salt doesn’t currently change, it uses the same value to re-hash the new password).

Are you seeing that even after filling out the form and submitting that the hashedPassword doesn’t update in the database? Are you using the standard generated dbAuth forgot/reset password pages, or do you have something custom going on?

Just an update - here’s how I accomplished what I needed:
in the user service, I modified createUser like so:

export const createUser = async ({ input }) => {
  requireAdmin();

  const { data, token } = initUser(input);

  const user = await db.user.create({
    data,
  });

  await onboardUser(user, token);

  return user;
};

and I added these functions to authHelpers:

export const initUser = (input) => {
  const tokenExpires = new Date()
  tokenExpires.setSeconds(
    tokenExpires.getSeconds() + 60 * 60 * tokenExpireHours
  )
  let token = md5(uuidv4())
  const buffer = Buffer.from(token)
  token = buffer.toString('base64').replace('=', '').substring(0, 16)

  const tokenHash = hashToken(token)

  const [hashedPassword, salt] = hashPassword(
    crypto.randomBytes(20).toString('hex')
  )

  const data = {
    ...input,
    hashedPassword,
    salt,
    resetToken: tokenHash,
    resetTokenExpiresAt: tokenExpires,
  }

  return { data, token }
}

export const onboardUser = (user, token) => {
  user.resetToken = token
  // call user-defined handler in their functions/auth.js
  return sendReset(sanitize(user), true)
}

This makes it so that when I create a new user, it inits them with an random password and sends them an email like the ‘forgot password’ flow.

I also modified sendReset to take an second boolean argument ‘newUser’, so I can modify the message based on whether it is being called by the newUser flow or the forgotPassword flow.

Most of this code is lifted from the forgotPassword method DbAuthHandler. Again, if the token creation code could be refactored into its own method, it would make this sort of implemention easier and more maintainable.

2 Likes