# Change Password (Self-Service) — Design **Date:** 2026-06-23 **Status:** Approved, pending implementation plan ## Summary Add a self-service "change password" feature so authenticated users can change their own password. The UI lives on a new **Account Settings** page in the user portal, reachable from the user-header menu's currently-disabled **Settings** item. The backend exposes a new authenticated endpoint that verifies the current password, enforces the existing ASP.NET Identity password policy, and — on success — revokes the user's *other* sessions while keeping the current one active. Out of scope (explicitly deferred): admin-driven forced password change / "must change on first login" after an admin reset. The existing admin reset endpoint (`POST /api/users/{id}/reset-password`) is unchanged. ## Existing infrastructure (context) - **User entity:** `API/ROLAC.API/Entities/AppUser.cs` — `IdentityUser`, so `PasswordHash` / `SecurityStamp` are inherited. - **Hashing & policy:** ASP.NET Core Identity (`PasswordHasher`, PBKDF2). Policy in `API/ROLAC.API/Program.cs`: min length 8, requires digit, uppercase, lowercase, and non-alphanumeric. - **Auth service / controller:** `API/ROLAC.API/Services/AuthService.cs`, `API/ROLAC.API/Controllers/AuthController.cs` (`/api/auth/login`, `/refresh`, `/me`, `/logout`). Refresh tokens stored in DB; the active refresh token is delivered in an HttpOnly cookie. - **Current user id:** read from the `sub` JWT claim (`ClaimTypes.NameIdentifier ?? "sub"`), because `MapInboundClaims = false` and `NameClaimType = "sub"`. - **Audit:** `API/ROLAC.API/Services/Logging/AuditLogger.cs` — security actions (login success/failure, logout) are logged; password change will be too. - **Frontend auth:** `APP/src/app/shared/services/auth.service.ts`. - **User portal:** `APP/src/app/portals/user-portal/` with `components/user-header/user-header.component.ts` (user menu with disabled Profile/Settings stubs). Routes in `APP/src/app/app.routes.ts` carry `title`/`titleZh`/`section` data for the unified system header. - **Form patterns:** `APP/src/app/features/users/components/edit-user-dialog/` (Kendo `kendo-textbox`, `kendo-formfield`, `kendo-formerror`, Reactive Forms). Form layout via Tailwind utilities on a neutral wrapper, not per-component SCSS. ## Backend ### Endpoint `POST /api/auth/change-password` — requires authentication. ```csharp public record ChangePasswordRequest(string CurrentPassword, string NewPassword); ``` ### Flow (`AuthService.ChangePasswordAsync`) 1. Resolve the user id from the `sub` claim; `UserManager.FindByIdAsync`. 2. `UserManager.ChangePasswordAsync(user, currentPassword, newPassword)`. This: - verifies the current password, - enforces the configured Identity password policy on the new password, - re-hashes and persists, - automatically bumps `SecurityStamp`. No manual `CheckPasswordAsync` call is needed. 3. On failure, return `400 Bad Request` with readable error messages: - wrong current password → "Incorrect current password", - policy failures → mapped to readable messages. 4. On success: - revoke all of the user's refresh tokens **except** the one presented in the current request's HttpOnly cookie (other devices get logged out; current session stays alive), - write a security audit-log entry (same pattern as login logging: action, category Security, entityId = user id, user email, IP). 5. Return `204 No Content`. The controller reads the current refresh token from the cookie to identify which session to preserve, and passes it to the service. Validation is server-authoritative; client-side checks are UX-only. ## Frontend ### Route & navigation - New route `/user-portal/account` → `AccountSettingsPageComponent`, registered in `app.routes.ts` with `[AuthGuard]` and `data: { title: 'Account Settings', titleZh: '帳戶設定', section: 'Account' }` so it uses the unified system header. - Wire the disabled **Settings** item in `user-header.component.ts` to navigate to this route (remove `disabled`, add navigation). The **Profile** stub is left as-is. ### Components - `AccountSettingsPageComponent` — page shell hosting a "Change Password" section/card. Room to grow later (profile, language preference). - `ChangePasswordFormComponent` — focused child component owning the form (single responsibility; independently testable). ### Form - Reactive Forms + Kendo, mirroring `edit-user-dialog` patterns. - Three `kendo-textbox type="password"` fields: Current password, New password, Confirm new password. - Layout via Tailwind utilities (`grid grid-cols-1`) on a neutral wrapper div — no per-component SCSS for layout. - Validators (UX-only; server stays authoritative): - new password: min 8, at least one upper, lower, digit, non-alphanumeric, - cross-field: new ≠ current, confirm === new. - Show password-rule hints and `kendo-formerror` messages. Submit disabled while invalid or pending. - Submit → `authService.changePassword(current, next)`: - `204` → success notification, reset form, - `400` → surface server message inline (e.g. incorrect current password). - Single narrow column → inherently mobile-friendly; no grid/card-list split. ### Auth service Add `changePassword(currentPassword, newPassword): Observable` calling `POST /api/auth/change-password` with `withCredentials: true` so the refresh-token cookie is sent. ## Testing Follow TDD — tests first, then implementation — for both layers. ### Backend (xUnit; build with `-c Release` per build env notes) - Success: correct current password + policy-valid new password → succeeds, stored hash changes, `204`. - Wrong current password → `400`, password unchanged. - New password failing policy (too short / missing a required class) → `400` with the relevant message. - Other refresh tokens revoked; the current cookie's refresh token preserved. - Audit entry written. ### Frontend (Karma/Jasmine) - `ChangePasswordFormComponent`: validators (weak password invalid, mismatch invalid, new === current invalid); submit disabled when invalid; calls service with correct args; renders server error on `400`; resets on success. - `AuthService.changePassword`: issues `POST /api/auth/change-password` with `withCredentials`. ## Deliverables checklist - Backend: `ChangePasswordRequest` DTO, `AuthController` action, `AuthService` method, refresh-token revocation (preserve current), audit logging. - Frontend: `/user-portal/account` route, `AccountSettingsPageComponent`, `ChangePasswordFormComponent`, Settings menu wiring, `authService.changePassword`. - Tests: backend unit tests + frontend unit tests.