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

241 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```csharp
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:
```csharp
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.
---
## 4. Recommended mechanism: ASP.NET Identity's built-in reset token
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.