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:
- Users are created with a default password — This allows them to log in immediately without completing the email verification step
- 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:
- Users should set their own password — Sending a default password (even temporarily) is a security risk
- Email verification should be mandatory — Users should not be able to log in until they verify their email
- 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:
- Navigate to Auth0 Dashboard → Actions → Library
- Create a new Action and name it "Block Unverified Email Login"
- Select "Login / Post-Login" as the trigger
- 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");
}
};
- Deploy the Action
- 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:
- Create users with
email_verified: false via the Management API
- Send an email verification email (not a password reset email) using the Management API endpoint:
POST /api/v2/jobs/verification-email
- User clicks the verification link in the email, which verifies their email and redirects them to your app
- Implement a Post-Login Action to block login until
email_verified === true
- 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
- Navigate to Auth0 Dashboard → Branding → Email Templates
- Select "Verification Email"
- Customize the template to look like an invitation email using metadata properties
- 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
- Go to Actions → Flows → Login
- Add the Post-Login Action to the flow
Important Considerations:
- 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.
- 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.
- 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.
- 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.
- 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