Prevent Password Reset for unverified user email accounts

I am building an authentication flow for an invite-only platform. When a user is invited, they are sent a custom repurposed Password Reset email with special language welcoming them to the platform. However, there is a problem. On this platform, the invitations need to expire after 72 hours. But, it is possible for a user to go to the Login page and trigger their own Password Reset email, which would send them an email with a new (not expired) link to reset their password. Upon resetting their password, they would gain access to the platform. However, we want to force the user to have a verified user in the system trigger another invitation email for the user. This is for security reasons in case the user email became compromised and shouldn’t receive another invite.

In my search for a solution, I came across this post Prevent send reset password email until the email is not verified

But it doesn’t seem to fully cover my use-case. In that user’s post, I think they are using verification emails, which I do not want to use, because it would be a poor user experience for a user to be invited with a verification email, only to then have to trigger their own password reset to gain access to the system.

My solution is to use a custom flag on the user’s app_metadata to control the email they are sent.

{% if user.email_verified == true %}
    # Password Reset Email
{% elsif user.app_metadata.block_unverified_pw_reset == false %}
    # Invitation Email
{% else %}
    # Invalid Password Reset Email - complete your invitation email, or contact your admin to send another invite
{% endif %}

When I want to send a user their invitation email, I create the user with the block_unverified_pw_reset set to false on the backend api I control.

  const unknownUser = await managementClient.createUser({
    connection: 'Username-Password-Authentication',
    password: generatePassword(),
    verify_email: false,
    email: userAwaitingCreation.email,
    email_verified: userAwaitingCreation.email_verified,
    app_metadata: {
       app_user_id: 'someId',
       block_unverified_pw_reset: false,
    },
  });

Then I send the password reset email, and update the user to toggle the block_unverified_pw_reset back to true

  await authenticationClient.requestChangePasswordEmail({
    email: userAwaitingInvite.email,
    connection: 'Username-Password-Authentication',
    client_id: props.auth0ClientId,
  });
  const unknownUser = await managementClient.updateUser(
    { id: userAwaitingInvite.user_id },
    {
      app_metadata: {
        ...userAwaitingInvite.app_metadata,
        block_unverified_pw_reset: true,
      },
    }
  );

Then, if an unknown actor attempts to reset their password from the Login page before they have reset their password from the invitation email, they receive an “Invalid Password Reset” email.

If I need to resend their invitation, I do more or less the same thing as when I create the users, where I update the user to set the block_unverified_pw_reset flag to false, send the reset password request, then update the block_unverified_pw_reset flag back to true.

This works in my testing, but I worry that this approach has race conditions if the requestChangePasswordEmail goes into a queue, and the user data isn’t stored at the time the request is received and put in the queue. In that scenario, it would be possible to accidentally send an “Invalid Password Change” email when I intended to send an “Invitation” email.

To elaborate, by the time the email queue processes the request, the user already has the block_unverified_pw_reset flag set back to true, and so would send the wrong email.

Again, I haven’t experienced this yet, so I want to post here to gut check this approach.

Thank you.

1 Like

Still awaiting a reply on this. Just want to make sure I haven’t made a footgun for myself here.

1 Like