Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.6 KiB
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, soPasswordHash/SecurityStampare inherited. - Hashing & policy: ASP.NET Core Identity (
PasswordHasher<AppUser>, PBKDF2). Policy inAPI/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
subJWT claim (ClaimTypes.NameIdentifier ?? "sub"), becauseMapInboundClaims = falseandNameClaimType = "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/withcomponents/user-header/user-header.component.ts(user menu with disabled Profile/Settings stubs). Routes inAPP/src/app/app.routes.tscarrytitle/titleZh/sectiondata for the unified system header. - Form patterns:
APP/src/app/features/users/components/edit-user-dialog/(Kendokendo-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.
public record ChangePasswordRequest(string CurrentPassword, string NewPassword);
Flow (AuthService.ChangePasswordAsync)
- Resolve the user id from the
subclaim;UserManager.FindByIdAsync. 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 manualCheckPasswordAsynccall is needed.
- On failure, return
400 Bad Requestwith readable error messages:- wrong current password → "Incorrect current password",
- policy failures → mapped to readable messages.
- 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).
- 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 inapp.routes.tswith[AuthGuard]anddata: { title: 'Account Settings', titleZh: '帳戶設定', section: 'Account' }so it uses the unified system header. - Wire the disabled Settings item in
user-header.component.tsto navigate to this route (removedisabled, 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-dialogpatterns. - 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-formerrormessages. 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<void> 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) →
400with 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 on400; resets on success.AuthService.changePassword: issuesPOST /api/auth/change-passwordwithwithCredentials.
Deliverables checklist
- Backend:
ChangePasswordRequestDTO,AuthControlleraction,AuthServicemethod, refresh-token revocation (preserve current), audit logging. - Frontend:
/user-portal/accountroute,AccountSettingsPageComponent,ChangePasswordFormComponent, Settings menu wiring,authService.changePassword. - Tests: backend unit tests + frontend unit tests.