Files
HIPCTF2/docs/guides/change-password.md
T
2026-07-21 22:26:44 +00:00

10 KiB

type, title, description, tags, timestamp
type title description tags timestamp
guide Change Password How an authenticated user changes their own password from the shell, and what each error branch looks like.
guide
change-password
auth
tester
2026-07-21T22:19:08Z

When this view is available

The change-password modal is part of the authenticated shell. It is opened from the user menu (trigger data-testid="user-menu-trigger" → entry data-testid="user-menu-change-password").

Layer File Check
Client route frontend/src/app/app.routes.ts The shell (/) uses authGuard.
Client modal frontend/src/app/features/shell/change-password/change-password-modal.component.ts Rendered inside the shell by HomeComponent.
Server route backend/src/modules/auth/auth.controller.ts POST /api/v1/auth/change-password — JWT-protected, CSRF-enforced.

How to access (tester steps)

  1. Sign in via /login.
  2. Click the username dropdown in the shell header (data-testid="user-menu-trigger").
  3. Click Change password (data-testid="user-menu-change-password").
  4. The modal overlay appears (data-testid="change-password-overlay") with the modal card (data-testid="change-password-modal").

Expected behavior

Inputs (mode = self)

Field Selector Constraints
Old password [data-testid="cp-old"] Required when mode === 'self'. Hidden in mode === 'admin-reset' (future Job).
New password [data-testid="cp-new"] Required. Must satisfy the server-side validatePassword (length + mixed-case per PASSWORD_MIN_LENGTH / PASSWORD_REQUIRE_MIXED).
Confirm new password [data-testid="cp-confirm"] Required. Must equal newPassword (group-level passwordMatchValidator).
Policy hint [data-testid="cp-policy"] Renders passwordPolicy.description from the bootstrap payload (e.g. "At least 12 characters with upper, lower, digit and symbol").
Cancel button [data-testid="cp-cancel"] Disabled while submitting. Emits cancel.
OK button [data-testid="cp-ok"] Disabled while submitting. Submits the form.

The modal closes on Escape (@HostListener('document:keydown.escape')) and on overlay click (the card stops propagation). Cancel is blocked while submitting() is true.

Inline validation (client-side)

State Selector Message
newPassword and confirmNewPassword differ [data-testid="cp-form-error"] Passwords do not match
API error returned [data-testid="cp-error"] Mapped via formatChangePasswordError() (see below).

Successful response

Outcome Visible behaviour
HTTP 204 Modal closes (changePasswordOpen.set(false)), user stays signed in on this tab. All other refresh tokens for the user are revoked server-side — other tabs will be forced to /login on their next guard tick.

Error envelope

The frontend maps each error code to user-facing copy via formatChangePasswordError({code, message}) in frontend/src/app/features/shell/change-password/password-feedback.ts:

HTTP code Modal message
400 PASSWORDS_DO_NOT_MATCH "New password and confirmation do not match"
400 PASSWORD_POLICY The server's policy text (or "New password does not meet the policy requirements" if empty).
400 VALIDATION_FAILED The server's policy text (fallback "Failed to change password").
401 INVALID_OLD_PASSWORD "Old password is incorrect"
401 UNAUTHORIZED "You must be signed in to change your password"
other (anything else) Server message (or fallback "Failed to change password").

Visual elements

Element Selector Purpose
Modal overlay [data-testid="change-password-overlay"] Backdrop; clicking it cancels.
Modal card [data-testid="change-password-modal"] Stops propagation so card clicks don't cancel.
Form error (match) [data-testid="cp-form-error"] Inline error from passwordMatchValidator.
Server error [data-testid="cp-error"] Inline error from formatChangePasswordError().

Architecture map

Step Where What happens
1 frontend/src/app/features/shell/change-password/change-password-modal.component.ts Dumb modal with reactive form, passwordMatchValidator, policy hint, errorMessage/submitting inputs.
2 frontend/src/app/features/shell/change-password/change-password-modal.validators.ts passwordMatchValidator group validator + PasswordPolicy zod schema.
3 frontend/src/app/features/shell/change-password/password-feedback.ts formatChangePasswordError({code, message}) — pure helper unit-tested in tests/frontend/change-password-modal.spec.ts.
4 frontend/src/app/features/home/home.component.ts Smart shell: owns changePasswordOpen/changePasswordSubmitting/changePasswordError signals; on submit(payload) calls AuthService.changePassword().
5 frontend/src/app/core/services/auth.service.ts changePassword(dto) runs ensureCsrf() then POST /api/v1/auth/change-password with withCredentials: true; returns ChangePasswordResult.
6 backend/src/modules/auth/dto/auth.dto.ts ChangePasswordDtoSchema (zod) — confirmNewPassword refines to === newPassword.
7 backend/src/modules/auth/auth.service.ts changePassword(userId, dto) (transaction): mismatch check → argon2.verify(old)validatePassword(new) → re-hash + UPDATE refresh_token SET revokedAt = now WHERE userId = ? AND revokedAt IS NULL.
8 backend/src/common/utils/password-policy.ts validatePassword(password, config) enforces PASSWORD_MIN_LENGTH and (when enabled) the mixed-case requirement.

Tester flows

Happy path

  1. Sign in. Open the user menu → Change password.
  2. Enter the current password, a new password meeting the policy, and the same new password in confirmation.
  3. Click OK. The modal closes; the user remains signed in.
  4. Sign-in from another browser session using the same account — the new password works; the previous password no longer works.

Wrong old password

  1. Sign in, open the modal.
  2. Enter an incorrect "Old password", a valid "New password", and the same new password in confirmation.
  3. Click OK. The modal stays open and the inline error reads "Old password is incorrect". No DB write happens.

Mismatched confirmation

  1. Sign in, open the modal.
  2. Enter the current password, a valid new password, and a different confirmation. The cp-form-error element appears immediately with "Passwords do not match" (the OK button stays disabled until the values match because the reactive form is invalid).
  3. Fix the confirmation. The error disappears.

Weak new password

  1. Sign in, open the modal.
  2. Enter the current password and a too-short new password (e.g. "abc"). The modal submits; the server returns 400 with code=PASSWORD_POLICY; the inline error reads the server-supplied policy message.

Unauthenticated

  1. Manually clear the session token in DevTools and submit the modal. The server returns 401 UNAUTHORIZED; the inline error reads "You must be signed in to change your password". The frontend then has no usable session, so the next guard tick (e.g. clicking a tab) sends the user to /login.

Notes

  • The change-password modal always runs in mode='self' from the shell. The mode='admin-reset' input (where the oldPassword field is hidden) is supported by the modal component and will be wired by the admin-reset Job.
  • The server revokes all refresh tokens for the user on success, so other devices must re-authenticate. The local sessionStorage session on this tab is preserved because the response is 204 (no new tokens are issued).

See also