Skip to content

feat(users): add self-service account unlock via email#187

Merged
antosubash merged 3 commits into
mainfrom
emdash/feat-self-service-account-unlock-email-summary-181-hhs5c
May 10, 2026
Merged

feat(users): add self-service account unlock via email#187
antosubash merged 3 commits into
mainfrom
emdash/feat-self-service-account-unlock-email-summary-181-hhs5c

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

  • Adds a self-service unlock flow: locked-out users can request an unlock link by email, and clicking it rotates their security stamp and clears the lockout. Closes Identity: account lockout self-service unlock via email #181.
  • Endpoints (SendUnlockEmail, SendUnlockEmailConfirmation, UnlockAccount) gate on IsEmailConfirmedAsync + IsLockedOutAsync, return identical error messages on every failure path to prevent user enumeration, and never throw on malformed input.
  • POST is rate-limited under the existing auth-strict policy (10/min/IP); GET pages and the confirmation redirect are unauthenticated.

Notable details

  • IAccountUnlockEmailSender contract lives in SimpleModule.Users.Contracts with a ConsoleAccountUnlockEmailSender fallback in the Users module and a real IdentityAccountUnlockEmailSender registered by the Email module.
  • UserSelfUnlockedEvent is published on successful unlock; auditing flows through the existing AuditingMessageBus decorator, so no extra handler is required.
  • Token purpose "AccountUnlock" is centralised in UsersConstants.TokenPurposes so both endpoints can't drift.
  • Decoder uses Base64Url.DecodeFromChars with OperationStatus, which is genuinely non-throwing on invalid input (unlike TryDecodeFromChars).

Test plan

  • dotnet test — all 17 projects pass (52 in Users.Tests, including 12 new integration tests).
  • npx biome check . — clean.
  • npm run validate-pages — new Inertia component names registered in Pages/index.ts.
  • npm run validate:i18n, npm run typecheck, framework-scope validation — clean.
  • dotnet build SimpleModule.slnx — 0 errors, 0 warnings.
  • Manual smoke (CI Playwright): visit /Identity/Account/Lockout → follow the unlock link → confirm 200 page + login works.

When users get locked out from failed sign-ins, they can now request an
unlock link by email instead of waiting or contacting an admin.

- Add SendUnlockEmailEndpoint (POST with rate limiting, no email enumeration leak)
- Add UnlockAccountEndpoint (token verification, security stamp rotation)
- Add IAccountUnlockEmailSender with console and production implementations
- Add UserSelfUnlockedEvent for audit trail
- Update Lockout page with link to unlock flow
- Add integration tests for happy path, unknown email, invalid token

Closes #181
- Unify error messages and add base64 decode guard in UnlockAccount to
  close a user-enumeration leak and prevent 500s on malformed input.
- Require confirmed email in SendUnlockEmail, matching ForgotPassword.
- Extract the AccountUnlock token-purpose string to a constant so the
  two endpoints can't drift.
- Use the typed routes helper in Lockout.tsx instead of a hardcoded path.
- Strengthen integration tests: recording email-sender fake, branches
  for healthy/unconfirmed/locked users, malformed-base64 guard, rate-
  limit smoke test, and GET-page smoke tests for the new pages.
- Re-format auto-generated routes.ts to satisfy biome.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 10, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 37393d7
Status: ✅  Deploy successful!
Preview URL: https://1857c7cc.simplemodule-website.pages.dev
Branch Preview URL: https://emdash-feat-self-service-acc.simplemodule-website.pages.dev

View logs

@antosubash antosubash merged commit 80fb818 into main May 10, 2026
6 checks passed
@antosubash antosubash deleted the emdash/feat-self-service-account-unlock-email-summary-181-hhs5c branch May 10, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identity: account lockout self-service unlock via email

1 participant