Files
deepdrft/product-notes/authblocks-password-reset-brief.md
daniel-c-harvey 042641d841 docs: expand Phase 19 to all three AuthBlocks registration paths + reset brief
Cover admin provision-now, public self-service redeem, and admin invite-by-email across CMS + public-site tracks. Add standalone AuthBlocks password-reset team brief.
2026-06-19 19:18:53 -04:00

14 KiB
Raw Permalink Blame History

Team Brief — Email-Backed Password Reset for AuthBlocks

Audience: an orchestrator (and its implementers) working only in the AuthBlocks repository at C:\Development\AuthBlocks. You do not need, and should not assume, any knowledge of the products that consume AuthBlocks. Everything you need is in this brief or in that repo.

Status: scoped request, not yet started. Author: product-designer (for a downstream consumer team). Date: 2026-06-19.


1. The goal in one sentence

Replace the non-functional "Reset Password" stub on the AuthBlocks user-administration Users page with a real, email-backed password-reset flow — so that triggering "Reset Password" for a user sends that user an email containing a secure, time-limited reset link, and following the link lets them set a new password.

This is an upstream library feature, delivered entirely inside AuthBlocks and published as a normal version bump. Consumers pick it up by referencing the new package version.


2. Where the stub lives today

AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor — the user grid has a per-row Reset Password MudButton whose handler is empty:

private async Task ResetPassword(UserInputModel? item)
{
    // todo integrate with email for secure reset
}

There is no backing API endpoint for this action. AuthBlocksLib/Routes/AuthRoutes.cs maps login, register, admin-register, refresh, logout, me, roles — and nothing for password reset. So this is a build-from-scratch flow on both the API side (new endpoints) and the Web side (wire the button + add a public reset page), reusing AuthBlocks' existing email and token machinery.


3. What AuthBlocks already has that you should reuse

The pending-registration flow is your template. AuthBlocks already does almost exactly this shape of work for invitations — generate a secure token, email a link, validate the token when the user returns. Read it end-to-end before designing reset; you are building a sibling flow:

  • Email sending is real and wired. AuthBlocksLib/AuthBlocksExtensions.cs (~line 109) registers services.AddScoped<IGeneralEmailSender, MailtrapEmailSender>();. The IGeneralEmailSender abstraction and MailtrapEmailSender implementation come from the shared NetBlocks library (namespace API.Common.Email.Mailtrap). The send signature in use is:

    await emailSender.SendEmailAsync(toAddress, cc: null, subject, htmlBody);
    

    See it called for real at AuthBlocksLib/Routes/PendingRegistrationRoutes.cs:124.

  • Email connection config. The host populates AuthBlocksOptions.EmailConnection (a NetBlocks EmailConnection with Host + Token) plus ApplicationName and SupportEmail when it calls AddAuthBlocks(options => { ... }). Those flow into AuthBlocksExtensions and are available to your reset endpoint exactly as they are to the registration endpoint. You do not need to invent any new config or sender — reuse IGeneralEmailSender and AuthBlocksOptions.

  • An HTML email template pattern. AuthBlocksLib/Common/RegistrationEmailTemplate.cs is a static Create(token, link, applicationName, supportEmail) returning a styled HTML string. Build a sibling PasswordResetEmailTemplate.Create(...) in the same file's neighbourhood and the same house style (the registration template is teal-branded, table-layout, support-line-collapses-when-empty — match it). Do not reuse the registration template verbatim; the copy is invitation-specific.

  • A token service pattern. AuthBlocksLib/Services/RegistrationTokenService.cs generates a random token, SHA-256-hashes {email}::{token}, persists the hash with a 7-day expiry, and validates / consumes it. However — for password reset, prefer ASP.NET Identity's built-in reset token (see §4) rather than re-implementing this hand-rolled scheme. The registration token service is a style reference for endpoint shape and email-link construction, not necessarily the token mechanism.

  • The deep-link construction idiom. The registration flow builds its link with QueryHelpers.AddQueryString(returnHost, { UserEmail, RegistrationToken }) and the public register page reads those query params and pre-fills (Register.razor, [SupplyParameterFromQuery]). Mirror this for the reset page: link carries email + resetToken; the reset page reads them.

  • Identity is fully present. UserService wraps UserManager<ApplicationUser> (see AuthBlocksData/Services/UserService.cs). UserManager gives you the canonical reset primitives — use them.


Password reset is a solved problem in ASP.NET Identity, and rolling your own token store for it is an avoidable security surface. Strong recommendation: use UserManager<ApplicationUser>'s built-in reset tokens rather than the hand-rolled RegistrationTokenService SHA-256 scheme.

  • var token = await userManager.GeneratePasswordResetTokenAsync(user); — produces a token bound to the user's security stamp; invalidated when the password changes or the stamp rotates.
  • var result = await userManager.ResetPasswordAsync(user, token, newPassword); — validates and applies in one call; enforces the configured password policy.
  • Token lifetime is governed by DataProtectionTokenProviderOptions.TokenLifespan (default 1 day) — confirm/configure to a sensible reset window (recommend 12 hours for reset, tighter than the 7-day registration window).

This means you likely do not need a new DB table or migration for reset (unlike registration, which persists pending rows). Confirm whether the default token providers are registered in the AuthBlocks Identity setup; if AddDefaultTokenProviders() (or equivalent) is not already called in the Identity configuration, add it — that is the one wiring prerequisite for GeneratePasswordResetTokenAsync to work.

Alternative considered (and not recommended): extend RegistrationTokenService / PendingRegistration into a generic token table that also serves reset. Rejected — it couples two unrelated flows, re-implements what Identity already does correctly, and adds a migration for no benefit. Use it only if there is a hard reason the Identity token provider cannot be enabled in this setup.


5. The surfaces to build

Three pieces, mirroring the registration flow's API-endpoint + email-template + web-page triad.

5.1 API endpoints (AuthBlocksLib/Routes/AuthRoutes.cs)

Add to the api/auth group. Two endpoints, both unauthenticated (a user resetting a forgotten password is by definition not logged in — the admin "Reset Password" button triggers the first of these on the user's behalf, but the endpoint itself authenticates via the token, not a bearer):

  1. POST api/auth/forgot-password — body { email, returnHost }. Looks up the user; if found, generates a reset token and emails the reset link ({returnHost}?email=&resetToken=). Always return success regardless of whether the email exists — do not leak account existence (a known reset-flow security requirement; the registration flow's "user already exists" message is acceptable for an admin-gated invite but a public forgot-password must not reveal it). On email-send failure, log and return a generic failure.

  2. POST api/auth/reset-password — body { email, resetToken, newPassword }. Resolves the user, calls ResetPasswordAsync(user, token, newPassword), returns the Identity result mapped to the AuthBlocks Result/ApiResult convention (see how Register maps results in AuthRoutes.cs).

Follow the existing AuthRoutes conventions exactly: ApiResult<T> / ApiResultDto<T> wrapping, ILogger<AuthLogger> for logging, Results.Ok / Results.BadRequest / Results.Json(..., 500) shapes.

5.2 Email template (AuthBlocksLib/Common/PasswordResetEmailTemplate.cs)

New static Create(resetLink, applicationName, supportEmail) in the visual style of RegistrationEmailTemplate. Reset copy: a clear "you (or an admin) requested a password reset," the CTA button to the reset link, an expiry notice matching the token lifespan, and "ignore this email if you didn't request it." No registration code box — reset uses an opaque token in the link, not a user-typed code (recommended; do not show the Identity token as a copy-paste code — it is long and URL-encoded).

5.3 Web surfaces (AuthBlocksWeb)

  • Wire the admin button. In Users.razor, replace the empty ResetPassword handler with a call to an IAuthApiClient (or the appropriate existing client) method that hits POST api/auth/forgot-password for item.Email, and show a confirmation (a StatusMessage / dialog: "Reset email sent to {email}"). This is the admin-initiated trigger.
  • Add a public reset page. New AuthBlocksWeb/Components/Pages/Account/ResetPassword.razor, @page "/account/reset-password", @rendermode InteractiveServer, no role gate (a forgotten-password user is unauthenticated). Read email + resetToken from query params (mirror Register.razor's [SupplyParameterFromQuery] pre-fill), present new-password + confirm fields, submit to POST api/auth/reset-password, and on success route to /account/login with a success message. Match Register.razor's form structure and validation idiom.
  • Optional: a public "forgot password?" entry. Consider a /account/forgot-password page (link from Login.razor) where a user enters their email to self-initiate reset — same forgot-password endpoint. Decide whether this is in scope or whether reset is admin-initiated only (see open questions).

5.4 Client method

Add the forgot-password / reset-password calls to whichever API client the Web project uses for auth (the registration/login flows go through JwtAuthenticationStateProvider / IAuthApiClient — follow the same pattern; do not introduce a new HTTP client).


6. Constraints

  • No account-existence leak on the public forgot-password path (§5.1).
  • Reuse, don't reinvent: IGeneralEmailSender for sending, AuthBlocksOptions for config, Identity's token provider for tokens, the existing Result/ApiResult conventions for endpoint returns, and the RegistrationEmailTemplate house style for the email.
  • Match the existing route + result conventions in AuthRoutes.cs precisely — this is a library; consumers rely on the shape staying idiomatic.
  • Versioning: this lands as a normal AuthBlocks version bump (packed/pushed by pack.ps1 like the other packages). Note the new version so consumers can pin to it.
  • Token lifespan for reset should be short (recommend 12 hours), distinct from the 7-day registration token.
  • Password policy is enforced by ResetPasswordAsync automatically — do not duplicate validation, but surface the Identity error messages back through the result.

7. Acceptance criteria

  1. Clicking "Reset Password" for a user on the Users admin page sends that user a styled email with a working reset link, and shows the admin a confirmation. No unhandled exception, no silent no-op.
  2. Following the reset link lands on /account/reset-password with the email pre-filled; setting a new password that meets policy succeeds and the user can immediately log in with the new password.
  3. An expired or tampered token is rejected with a clear, non-leaky error.
  4. The public forgot-password endpoint returns the same response whether or not the email maps to a real account (no existence leak).
  5. Email send is exercised through the real IGeneralEmailSender (Mailtrap in the configured environment) — verify an email actually arrives.
  6. No new required config beyond what AddAuthBlocks already accepts (reset reuses the existing email connection + application-name + support-email options). If a token-provider registration was missing, it is added and documented.
  7. Published as a version bump; the new version is recorded.

8. Open questions for the implementing team / its sponsor

  1. Admin-initiated only, or also public self-serve? Is the only entry point the admin "Reset Password" button (§5.3 first bullet), or do you also want a public "forgot password?" link from the login page (§5.3 last bullet)? The endpoints support both; the question is which Web surfaces to build. Recommendation: build both endpoints, ship the admin button now, and add the public forgot-password page in the same pass since it is nearly free once the endpoint exists.
  2. Token mechanism — confirm Identity's built-in is acceptable (§4 recommendation) vs. a hard requirement to use the hand-rolled hashed-token scheme. Recommendation: Identity built-in.
  3. Reset token lifespan — confirm the window (recommend 12 hours).
  4. Return host / link base — the registration flow has the caller pass returnHost. Confirm the reset flow does the same (the consumer supplies the base URL of its public reset page), vs. AuthBlocks configuring a reset base URL in options. Recommendation: pass returnHost per-call, mirroring registration, so AuthBlocks stays host-agnostic.
  5. Does the public reset page (/account/reset-password) need to render in a consumer's own layout? The page ships in the AuthBlocks RCL with no @layout, so it inherits whatever the consuming host sets as default — same as Register.razor. Confirm this is acceptable (it should be; it is how registration already behaves).

9. Suggested reading order in the repo

  1. AuthBlocksLib/Routes/PendingRegistrationRoutes.cs — the email-sending endpoint to mirror.
  2. AuthBlocksLib/Routes/AuthRoutes.cs — where your endpoints go; the result/logging conventions.
  3. AuthBlocksLib/Common/RegistrationEmailTemplate.cs — the email house style.
  4. AuthBlocksWeb/Components/Pages/Account/Register.razor — the public-page + query-param-prefill pattern for your reset page.
  5. AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor — the stub to replace.
  6. AuthBlocksLib/AuthBlocksExtensions.cs + AuthBlocksOptions.cs — the email sender + options wiring you reuse (and where to add a token-provider registration if one is missing).
  7. AuthBlocksData/Services/UserService.cs — the UserManager<ApplicationUser> access point for the Identity reset primitives.