Help with our Signup/Register flow and big security flaws

Have inherited an auth0 sign up flow that needs attention for a new client facing application. Users are created internally and sent to Auth0 via the API.

Our full flow looks like so:

  1. A User is created in internal system with an Id, email and phone number
  2. This user is created in Auth0 via the IManagamentAPIClient c# like so managementApi.Users.CreateAsync(clientRequest);
    It is created with a default password…. ( not user set )
  3. Once the user is created we then use the IAuthenciationApiClient to send a password reset email like so… This acts as the entry point into our system in the absense of a Register or Welcome email since the user is created in step 2.
    await authenticationApi.ChangePasswordAsync(new() { Connection = auth0Settings.Value.Connection, Email = user.Email });
    1. The change password ( Link ) email template is used here and we change the wording to make it look somewhat a welcome email ( using some metadata properties set )
  4. This email link has an expiry of a few days meaning if a user clicks on this link in the email after 2 days they are presented with a ‘Expired Link’ dialog.
  5. THE ISSUE is that at this point the user is able to navigate to the normal login screen and select ‘Forgot Password’ and essentially sign up without going through the email link at all. This basically means the expired link in the email is completely pointless and giving us no layer of security other than a dialog saying its expired.

So, a few questions…

How might we tweak our flow so that an expired ‘Sign up’ or ‘Register’ link completely blocks a user from logging in?

Are we creating an anti-pattern creating the users upfront and then sending them a reset password email as the initial email?

Happy to give more information if required but this is as concise I can be as of now.

as a matter of fact it seems we actually follow this guide from Auth0 Implement User Invitations for Application Sign-Up using Password Reset Emails - Auth0 Docs so i’d be interested if others have done the same and how they have solded my concerns

Hi @jharrison

Welcome to the Auth0 Community!

You have inherited an Auth0 sign-up flow where users are created internally with a default password, then sent a password reset email as their entry point. The security issue is that users can bypass the email verification link by navigating directly to the login screen and using "Forgot Password," making the expired link check pointless.

This is an anti-pattern. Auth0 recommends using the official Invite Users workflow instead, which allows you to invite users to set their own password and enforce email verification before login. To fix your current flow, implement a Post-Login Action that blocks login until the user has verified their email via the initial link.

[Root Cause]

Your current flow has two security gaps:

  1. Users are created with a default password — This allows them to log in immediately without completing the email verification step
  2. No enforcement of email verification — Auth0 does not automatically block login for unverified emails. Users can bypass the password reset link by using "Forgot Password" to set a new password themselves

The expired link in the email provides no real security because it only prevents password reset via that specific link — it does not prevent login or password reset via other methods.

Why This Is an Anti-Pattern:

  1. Users should set their own password — Sending a default password (even temporarily) is a security risk
  2. Email verification should be mandatory — Users should not be able to log in until they verify their email
  3. The invitation flow should be explicit — Users should understand they are being invited and must complete setup before accessing the system

Recommended Solution: Use Auth0's Invite Users Workflow

Auth0 provides an official Invite Users pattern specifically for this scenario. This is the recommended approach and is documented in Auth0's design guide for invite-only applications.

[Solution]

Option 1: Implement Post-Login Action to Block Unverified Users (Quick Fix)

If you want to keep your current flow but add security, implement a Post-Login Action that denies access to users who have not verified their email:

  1. Navigate to Auth0 Dashboard → Actions → Library
  2. Create a new Action and name it "Block Unverified Email Login"
  3. Select "Login / Post-Login" as the trigger
  4. Add the following code:
exports.onExecutePostChallenge = async (event, api) => {
  if (event.user.email_verified === false) {
    // Deny access to the user
    api.access.deny("Please verify your email by clicking the link in the invitation email");
    
    // Redirect to a custom error page (optional)
    api.redirect.sendUserTo("https://your-app.com/email-verification-required");
  }
};
  1. Deploy the Action
  2. Add to flow: Go to Actions → Flows → Login and add this Action to the flow

Important: When you create users via the Management API, ensure email_verified is set to false:

var createUserRequest = new UserCreateRequest
{
    Email = user.Email,
    Password = "TempPassword123!", // Temporary password
    EmailVerified = false, // This is the key
    Connection = "Username-Password-Authentication"
};
await managementApi.Users.CreateAsync(createUserRequest);

Option 2: Migrate to Auth0's Official Invite Users Workflow (Recommended)

This is the long-term, recommended approach:

  1. Create users with email_verified: false via the Management API
  2. Send an email verification email (not a password reset email) using the Management API endpoint: POST /api/v2/jobs/verification-email
  3. User clicks the verification link in the email, which verifies their email and redirects them to your app
  4. Implement a Post-Login Action to block login until email_verified === true
  5. Once verified, user can set their password via the standard password reset flow or a custom password setup page

Step-by-step implementation:

Step 1: Create the user with email_verified = false

var createUserRequest = new UserCreateRequest
{
    Email = user.Email,
    EmailVerified = false,
    Connection = "Username-Password-Authentication",
    AppMetadata = new Dictionary<string, object>
    {
        { "invited_at", DateTime.UtcNow }
    }
};
var createdUser = await managementApi.Users.CreateAsync(createUserRequest);

Step 2: Send verification email (not password reset)

var verificationEmailRequest = new VerificationEmailRequest
{
    UserId = createdUser.UserId,
    ClientId = "YOUR_CLIENT_ID"
};
await managementApi.Jobs.SendVerificationEmailAsync(verificationEmailRequest);

Step 3: Customize the verification email template

  1. Navigate to Auth0 Dashboard → Branding → Email Templates
  2. Select "Verification Email"
  3. Customize the template to look like an invitation email using metadata properties
  4. Use conditional logic to show different text based on user.app_metadata.invited_at

Step 4: Implement Post-Login Action to block unverified users

exports.onExecutePostLogin = async (event, api) => {
  if (!event.user.email_verified) {
    api.access.deny("Your email address is not verified. Please check your email for the verification link.");
  }
};

Step 5: Deploy the Action and add to Login flow

  1. Go to Actions → Flows → Login
  2. Add the Post-Login Action to the flow

Important Considerations:

  1. The onExecutePostChallenge trigger (used in Option 1) runs after the user has successfully authenticated. This means they will briefly see a login success before being denied. Use onExecutePostLogin instead for a cleaner experience.
  2. Email verification is persistent — Once email_verified is set to true, it remains true even if the user changes their password. This is the correct behavior.
  3. Password reset emails — If you continue to use password reset emails, users can still bypass email verification. The Post-Login Action is essential to prevent this.
  4. Multiple password reset links — Auth0 only allows one valid password reset link at a time. If a user requests a new reset, the old link becomes invalid. This is a security measure.
  5. Testing — After implementing the Post-Login Action, test by:
    • Creating a test user with email_verified: false
    • Attempting to log in without verifying email (should be denied)
    • Verifying the email and logging in again (should succeed)

We recommend implementing Option 2 (the official Invite Users workflow) for a production application, as it aligns with Auth0 best practices and provides the strongest security posture.

Kind Regards,
Nik

Hey @nik.baleca thanks for your reply, really helpful. I’m a little suprised that this flow is not catered for without such workarounds. It all feels like a bit of an afterthought I have to say. Is your suggested flow documented anywhere?

I understand your concept but it gets a bit blurry for me when you suggest that I need to trigger a ‘password change’ ticket once the user is verified? Would this be generated via API integration or a hook in Auth0 itself? If the answer is API, then how do I know the user has been verified to send a ‘password change’ ticket.

Secondly, you mention to attach an ‘email verification ticket’ inside the verification email with a result_url param. Is this just a redirect to a password change once the user has verified themselves? I’m a little confused by the 3rd point as it seems as though you’re suggesting to send a ‘password change’ ticket prior to the verification email and pass the general result_url into the verification email..

Apologies if that is all a bit wordy and thank you again

It all feels like a bit of an afterthought I have to say. Is your suggested flow documented anywhere?

Unfortunately not since the behaviour would be expected. If the user receive a pw reset email or if they sent one themselves via the login page, the outcome or process is fundamentally the same.

I understand your concept but it gets a bit blurry for me when you suggest that I need to trigger a ‘password change’ ticket once the user is verified? Would this be generated via API integration or a hook in Auth0 itself? If the answer is API, then how do I know the user has been verified to send a ‘password change’ ticket.

You would need to first generate a password change and email verification ticket via the Management API as I have linked above. The single link will both verify the user and trigger a password reset for them.

Secondly, you mention to attach an ‘email verification ticket’ inside the verification email with a result_url param. Is this just a redirect to a password change once the user has verified themselves? I’m a little confused by the 3rd point as it seems as though you’re suggesting to send a ‘password change’ ticket prior to the verification email and pass the general result_url into the verification email..

I understand your confusion and I will try to be as clear as possible.
Once the proper URL has been created for the Email Verification Ticket(which will include the Password Change Ticket as a result_url), you will need to send this URL to the user via email. The email template can be anything of your choosing, either a Welcome Email, Verification Email or Password Change Email should be good enough to use. The email itself will contain the verification ticket which upon being clicked, will immediately verify the user’s email and set email_verified to true and they will be redirected to the password change ticket where they will be prompted to set their new password. If the user attempts to reset the password outside of this flow, they will be redirected to a custom error page indicating that they need to set the password through the specific email.
Inside the email that you will be sending, there will not be different links for password change or verification, it will be a single url which both verifies the user and redirects them to the password change screen.

If you need any further clarification or some examples of the flow, please let me know!

Kind Regards,
Nik

Hi @nik.baleca - I have been following this discussion, but am in a tight spot. If I follow your suggestion, and create an action:

  exports.onExecutePostChallenge = async (event, api) => {
    console.log("[onExecutePostChallenge]: ", event);

    if(event.user.email_verified === false) {
      //Deny access to the user
      api.access.deny("Please reset your password through the invitation link");

      //Redirect to a custom error page informing the user
      api.redirect.sendUserTo("https://example.com/");
    }
  }; 

When user clicks on the link, it leads to invalid link auth0 link screen. Upon checking the logs, it seems to be error of type fcpr, with message Failed to complete password reset as client could not be identified. Any configured Actions will not execute. Please include client_id in the change password request..

Now Im very confused about this error. Im simply using this action, don’t have any other custom code.

Alright, indeed, the error seems quite weird. I will look into it and come back with an update!

Thanks,
Nik

Hi again @nahmadi

Did you generate a password change ticket with a client_id parameter included in the body? This error is not caused by the logic inside your Action, but rather by the existence of an Action in the flow combined with a missing parameter in your Ticket generation.

Also, it is important to mention that in my previous message regarding the PostChallenge action example, I had a complete oversight of the fact that you can either DENY access or REDIRECT the user.

I would suggest to redirect the user straight away since they will not reach the password reset screen anyways or you can customize the error screen inside Universal Login to inform the user of the reason they were denied access.

Kind Regards,
Nik

Hi @nik.baleca,

So, we are not calling the password change ticket from our service. i.e. we’re not using the management api.

We simply click on the link sent in the email (Change Password (Link)) from Auth0.

Got it @nahmadi!

Could you please let me know if you are using a custom email provider on your tenant or if you have used the default Auth0 one for your tests?

If you are using a custom email provider, have you configured the template with a Redirect To URL?

Kind Regards,
Nik

Yes, we are using Sendgrid, but we don’t use custom templates. The email template is what is defined by Auth0.

We’ve set the Redurect To in Auth0 Email to be {{ application.name }}

regards,

Got it, can you please tell me how are you testing the Change Password flow for the user? Are you selecting “Forgot Password” under the Universal Login flow of your application or are you sending the email differently (ex: Try button on email template).

Kind Regards,
Nik

Hi @nik.baleca, yes we’re using “Forgot Password” mostly, but the behaviour is still the same if Im just testing the Try button also.

Regards,

Got it.

By any chance, do you have an AD/LDAP configured on your tenant?

Kind Regards,
Nik

Hi @nik.baleca, I have checked and we don’t have any AD/LDAP configured.

Thanks

Hi again!

As far as I have checked, the specified error would be triggered only when using the “Test” button available under an email template within the Auth0 Dashboard. This happens because the “Test” button does not have a default Client ID and the error is expected. Also, this error can be encountered when trying to generate a change pw ticket without a Client ID passed inside the body of the request.

Otherwise, when testing with the Login Box option or with an /authorize call the behaviour was normal and I could not reproduce the mentioned error.

Can you confirm that this behaviour happens during a normal /authorize call or the Login Box option without triggering the endpoint for the pw change ticket?

Kind Regards,
Nik

Hya @nik.baleca,

Thanks for getting back to me on this. I have been looking into this for a while and I can confirm that this behavior is persistent in both flows. Either if Im testing:

  1. With “Try” button which allows me to trigger emails
  2. With Universal login experience. i.e. recieving the email to reset password by clicking on “forgot password”.

Would highly appreciate if someone can somehow look into our logs?

Regards,

I see.

Does the password change link work if you unbind the PostChallenge action by any chance?

Also, if you copy the URL sent in the email and paste it, does it contain the client_id parameter?

If you can send me a DM with the tenant name, I will take a look at your configuration.

Kind Regards,
Nik

Yes it works, because the PostChallenge action is removed.

I have checked and don’t see it.

Sure DMing you. Thanks