Summary
When a user gets locked out from too many failed sign-ins, give them a self-service path to unlock — a "Send unlock email" button that delivers a tokenized link, instead of forcing them to wait out LockoutEnd or contact an admin.
Why we need this
Today, account lockout is a one-way street for end users:
- User mistypes their password 5 times (or whatever
MaxFailedAccessAttempts is set to). - Identity sets
LockoutEnd and the user lands on LockoutEndpoint's "Account locked" page. - Their only options are: wait for the lockout window (default 5 minutes but configurable to much longer), or email support / an admin who calls
UnlockAccountAsync manually.
This is bad UX and bad security:
- Real users get punished for typos and have no agency to recover.
- Support gets spammed with "unlock my account" tickets.
- Admins handling unlock requests by email have no way to verify the requester actually controls the account — they're just trusting the email reply chain.
A token-based unlock email solves all three: the user gets immediate recovery, support workload drops, and verification ("can read email tied to the account") is actually stronger than the current admin verification process.
This is a standard Identity feature using primitives we already have — GenerateUserTokenAsync / VerifyUserTokenAsync with a custom IUserTwoFactorTokenProvider purpose, plus the existing IEmailSender plumbing.
How the user will use it
- User hits the lockout page (
/account/lockout). - Page now includes: "Forgot your password? You can also receive an unlock link by email."
- User enters their email, clicks "Send unlock link".
- Backend (
SendUnlockEmailEndpoint):
- Looks up the user. If not found or not currently locked, still returns 200 with the same generic message (don't leak which emails are registered or locked).
- If found and locked, generates an unlock token via
userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "Unlock"). - Sends an email with a link to
/account/unlock?userId=...&token=....
- User clicks link →
UnlockAccountEndpoint verifies the token, calls userManager.SetLockoutEndDateAsync(user, null) and ResetAccessFailedCountAsync. - User lands on a "Account unlocked" success page with a "Sign in" button.
Optional polish: rate-limit the "send unlock email" endpoint per IP and per email to prevent abuse (lots of unlock spam to a victim's inbox).
Implementation notes
- Two new endpoints in
modules/Users/src/SimpleModule.Users/Pages/Account/:
SendUnlockEmailEndpoint (POST) — generates token, sends email. Always returns the same generic confirmation regardless of whether the email exists.UnlockAccountEndpoint (GET, view) — verifies token, unlocks if valid, renders success/failure.
- Token purpose string:
"Unlock". Use the default token provider. - Reuse the existing
IEmailSender<ApplicationUser> and create a new email template / message. - Update
LockoutEndpoint page to include the "send unlock email" form alongside the existing wait-it-out copy. - Wire rate limiting via the existing
RateLimiting module: per-IP (e.g., 5/hour) and per-email (e.g., 3/hour). - Audit event
UserSelfUnlockedEvent so admins can see self-unlocks happening. - Tests:
- Locked user with valid token → unlock success.
- Unknown email → generic 200 (no enumeration leak).
- Tampered / expired token → fails closed.
- Rate limits engage on repeated calls.
Benefits
- Closes the most common support ticket type for any auth system.
- Self-verification via email is at least as strong as the current "trust the support email thread" pattern, with a much faster median resolution time.
- Costs the user one minute, costs the admin nothing.
- Aligns with how every modern auth system handles lockout.
Acceptance criteria
Summary
When a user gets locked out from too many failed sign-ins, give them a self-service path to unlock — a "Send unlock email" button that delivers a tokenized link, instead of forcing them to wait out
LockoutEndor contact an admin.Why we need this
Today, account lockout is a one-way street for end users:
MaxFailedAccessAttemptsis set to).LockoutEndand the user lands onLockoutEndpoint's "Account locked" page.UnlockAccountAsyncmanually.This is bad UX and bad security:
A token-based unlock email solves all three: the user gets immediate recovery, support workload drops, and verification ("can read email tied to the account") is actually stronger than the current admin verification process.
This is a standard Identity feature using primitives we already have —
GenerateUserTokenAsync/VerifyUserTokenAsyncwith a customIUserTwoFactorTokenProviderpurpose, plus the existingIEmailSenderplumbing.How the user will use it
/account/lockout).SendUnlockEmailEndpoint):userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "Unlock")./account/unlock?userId=...&token=....UnlockAccountEndpointverifies the token, callsuserManager.SetLockoutEndDateAsync(user, null)andResetAccessFailedCountAsync.Optional polish: rate-limit the "send unlock email" endpoint per IP and per email to prevent abuse (lots of unlock spam to a victim's inbox).
Implementation notes
modules/Users/src/SimpleModule.Users/Pages/Account/:SendUnlockEmailEndpoint(POST) — generates token, sends email. Always returns the same generic confirmation regardless of whether the email exists.UnlockAccountEndpoint(GET, view) — verifies token, unlocks if valid, renders success/failure."Unlock". Use the default token provider.IEmailSender<ApplicationUser>and create a new email template / message.LockoutEndpointpage to include the "send unlock email" form alongside the existing wait-it-out copy.RateLimitingmodule: per-IP (e.g., 5/hour) and per-email (e.g., 3/hour).UserSelfUnlockedEventso admins can see self-unlocks happening.Benefits
Acceptance criteria
LockoutEndandAccessFailedCount.