feat(auth): live per-rule password feedback + breach check on /register + /reset-password (BUNYIP-240) #265

Merged
YousifShkara merged 2 commits from feat/BUNYIP-240-inline-password-feedback into main 2026-06-29 10:41:57 +02:00
Owner

User feedback: clicking Create Account on /register and only then learning the password was rejected (with the field blanked for re-entry) is bad UX. Same on /reset-password. The fix is inline, in-page feedback while the user types.

Restructured pw_reqs() to render four <li> rows with stable IDs (pw-len, pw-case, pw-digit, pw-breach) and a leading indicator glyph slot. New inline password_live_validation_script() block attaches an input listener to the password field and toggles each row between pending (○) / pass (✓ teal) / fail (✗ destructive) on every keystroke. The first three rules mirror the server-side password_ok() rules verbatim (length 12, both cases, digit + special). The fourth row runs a HaveIBeenPwned k-anonymity lookup: SHA-1 the password client-side via Web Crypto, send only the first 5 hex chars to api.pwnedpasswords.com/range/, scan the response for the matching 35-char suffix. The full password never leaves the browser. Debounced 500ms so per-keystroke HIBP traffic is bounded.

Confirm-password field gets a live match/mismatch indicator under it. Create Account button is disabled until all four indicators are green AND the confirm matches; server-side password_ok() stays the non-JS backstop.

CSP connect-src extended in bunyip-web/src/security.rs with https://api.pwnedpasswords.com so the fetch isn't blocked, with a new unit test pinning the substring against future tightening.

Email value is preserved on server-side rejection so the user does not have to retype it (the password fields stay blank on re-render, which is the standard pattern - browsers refuse to render persisted password values for autofill safety anyway).

Same treatment applied to /reset-password confirm form (pw_reqs() is reused; the script is reusable as-is since it binds by ID).

#BUNYIP-240

User feedback: clicking Create Account on /register and only then learning the password was rejected (with the field blanked for re-entry) is bad UX. Same on /reset-password. The fix is inline, in-page feedback while the user types. Restructured `pw_reqs()` to render four `<li>` rows with stable IDs (`pw-len`, `pw-case`, `pw-digit`, `pw-breach`) and a leading indicator glyph slot. New inline `password_live_validation_script()` block attaches an `input` listener to the password field and toggles each row between pending (○) / pass (✓ teal) / fail (✗ destructive) on every keystroke. The first three rules mirror the server-side `password_ok()` rules verbatim (length 12, both cases, digit + special). The fourth row runs a HaveIBeenPwned k-anonymity lookup: SHA-1 the password client-side via Web Crypto, send only the first 5 hex chars to api.pwnedpasswords.com/range/<prefix>, scan the response for the matching 35-char suffix. The full password never leaves the browser. Debounced 500ms so per-keystroke HIBP traffic is bounded. Confirm-password field gets a live match/mismatch indicator under it. Create Account button is disabled until all four indicators are green AND the confirm matches; server-side `password_ok()` stays the non-JS backstop. CSP `connect-src` extended in bunyip-web/src/security.rs with `https://api.pwnedpasswords.com` so the fetch isn't blocked, with a new unit test pinning the substring against future tightening. Email value is preserved on server-side rejection so the user does not have to retype it (the password fields stay blank on re-render, which is the standard pattern - browsers refuse to render persisted password values for autofill safety anyway). Same treatment applied to /reset-password confirm form (pw_reqs() is reused; the script is reusable as-is since it binds by ID). #BUNYIP-240
feat(auth): live per-rule password feedback + breach check on /register + /reset-password (BUNYIP-240)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 22s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m43s
f2bc1325f6
User feedback: clicking Create Account on /register and only then learning the password was rejected (with the field blanked for re-entry) is bad UX. Same on /reset-password. The fix is inline, in-page feedback while the user types.

Restructured `pw_reqs()` to render four `<li>` rows with stable IDs (`pw-len`, `pw-case`, `pw-digit`, `pw-breach`) and a leading indicator glyph slot. New inline `password_live_validation_script()` block attaches an `input` listener to the password field and toggles each row between pending (○) / pass (✓ teal) / fail (✗ destructive) on every keystroke. The first three rules mirror the server-side `password_ok()` rules verbatim (length 12, both cases, digit + special). The fourth row runs a HaveIBeenPwned k-anonymity lookup: SHA-1 the password client-side via Web Crypto, send only the first 5 hex chars to api.pwnedpasswords.com/range/<prefix>, scan the response for the matching 35-char suffix. The full password never leaves the browser. Debounced 500ms so per-keystroke HIBP traffic is bounded.

Confirm-password field gets a live match/mismatch indicator under it. Create Account button is disabled until all four indicators are green AND the confirm matches; server-side `password_ok()` stays the non-JS backstop.

CSP `connect-src` extended in bunyip-web/src/security.rs with `https://api.pwnedpasswords.com` so the fetch isn't blocked, with a new unit test pinning the substring against future tightening.

Email value is preserved on server-side rejection so the user does not have to retype it (the password fields stay blank on re-render, which is the standard pattern - browsers refuse to render persisted password values for autofill safety anyway).

Same treatment applied to /reset-password confirm form (pw_reqs() is reused; the script is reusable as-is since it binds by ID).

#BUNYIP-240
merge: resolve security.rs conflict with BUNYIP-235 (form-action for Stripe)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 33s
Check / fmt + clippy + build + tests (pull_request) Successful in 22m22s
Create release / Create release from merged PR (pull_request) Has been skipped
0e7ba70b7c
BUNYIP-235 landed on main while this branch was open. Both PRs touch bunyip-web/src/security.rs: BUNYIP-235 widened `form-action` to allow Stripe Checkout + billing portal redirects; this branch extends `connect-src` with the HIBP k-anonymity endpoint for the live breach check. The only conflict was the test block - both PRs added a new `#[test]` directly after the existing `policy_includes_required_directives` test, and git could not pick an order. Keep both: HIBP test first (alphabetical with the doc-comment narrative this branch already added), Stripe test second.

The policy string itself auto-merged correctly (`form-action` carries Stripe destinations AND `connect-src` carries HIBP), and `just check-container` is green: every CSP test passes against the combined policy.

#BUNYIP-240
YousifShkara deleted branch feat/BUNYIP-240-inline-password-feedback 2026-06-29 10:41:57 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!265
No description provided.