Skip to content

Identity: account lockout self-service unlock via email #181

Description

@antosubash

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:

  1. User mistypes their password 5 times (or whatever MaxFailedAccessAttempts is set to).
  2. Identity sets LockoutEnd and the user lands on LockoutEndpoint's "Account locked" page.
  3. 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

  1. User hits the lockout page (/account/lockout).
  2. Page now includes: "Forgot your password? You can also receive an unlock link by email."
  3. User enters their email, clicks "Send unlock link".
  4. 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=....
  5. User clicks link → UnlockAccountEndpoint verifies the token, calls userManager.SetLockoutEndDateAsync(user, null) and ResetAccessFailedCountAsync.
  6. 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

  • Lockout page offers a "send unlock email" form.
  • Endpoint always returns a generic message (no account-existence leak).
  • Email contains a single-use, time-limited unlock link.
  • Clicking the link clears LockoutEnd and AccessFailedCount.
  • Endpoint is rate-limited per IP and per email.
  • Audit event emitted on successful self-unlock.
  • Integration tests for happy path, unknown email, invalid token, and rate-limit engagement.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions