> Synthetic example. Not a client engagement.

# SAMPLE-104 — Password reset link is single-use and expires after 30 minutes

**Story** · **Status** In Review · **Priority** High · **Assignee** Developer · **Reporter** QA Lead · **Opened** 2026-09-23

## Description

Customers who forget their password use "Forgot password?" on the sign-in page at app.example.test. The reset link arrives by email, may be used once, and expires 30 minutes after it is issued.

Out of scope: the email template and the password policy itself.

The replay defect found by the QA run is tracked as SAMPLE-105.

## Acceptance criteria

- [x] The reset link is sent only to the address on the account, and the response is identical whether or not that address is registered.
- [x] The link expires 30 minutes after it is issued.
- [ ] A link that has been used once is rejected on every later attempt.
- [x] Reset requests are rate-limited to five per hour per account.
- [ ] The pending reset is invalidated when the password changes in another session.
- [x] The new password must satisfy the password policy and must differ from the previous password.
- [x] Reset links are stored hashed and never written to application logs.

## QA verification

### Account recovery — password reset — 7/9 passed (2026-09-23)

Needs attention (2):

✗ **AC3** [negative] Reuse a consumed reset link
  Expected: The second attempt is rejected and no password change is recorded.
  Observed: The reset form is served again and the password changes.
  Steps:
    1. Request a reset for customer@example.test
    2. Complete the reset and keep the link
    3. Open the link again in a second browser
    4. Submit a different password
  Note: Recorded run attached to finding #1; screenshots with the run evidence.

⊘ **AC5** [negative] Change the password in another session, then reuse the link
  Expected: The pending reset is invalidated and the link is rejected.
  Observed: Not executed — the staging mail catcher was unavailable, so the second session could not be established.
  Steps:
    1. Request a reset
    2. Change the password from a signed-in session

## Findings (5)

### [critical] Reset link remains usable after it has been used once (review: confirmed)

One link completed a password reset twice, from two browsers, with no re-authentication.

**Impact:** Anyone who can read the link once — a shared inbox, a forwarded message, a browser history entry on a shared machine — can change the account password again without knowing the current one.

**Recommendation:** Invalidate the token on first successful use, bind it to the request that issued it, and invalidate every pending reset when the password changes.

_Found 2026-09-23_

### [high] Password change leaves other sessions signed in (review: confirmed)

After the password is changed from one session, a second session continues to work until it expires.

**Impact:** A session the customer is trying to revoke survives the action they took to revoke it.

**Recommendation:** Revoke all other sessions on password change and record it in the account history.

_Found 2026-09-23_

### [medium] Reset requests are rate-limited per address instead of per account

The five-per-hour limit is counted per source address, so attempts spread across addresses are not counted together.

**Impact:** A distributed attempt can exceed the intended per-account limit.

**Recommendation:** Count attempts per account and keep the per-address limit as a second gate.

_Found 2026-09-23_

### [medium] Reset accepts a password that differs from the previous one only by case (review: rejected)

Re-ran the scenario five times on the same build: the policy check rejects case-only variants. The first observation came from a stale build.

**Recommendation:** No change — the policy rule already covers this.

_Found 2026-09-23_

### [low] The reset confirmation page prints the account address in full

The confirmation step shows the full email address rather than a masked form.

**Impact:** Shoulder-surfing exposure on a shared screen; no functional risk.

**Recommendation:** Mask the address (c***@example.test) or omit it.

_Found 2026-09-23_

**Status history:** Backlog → To Do (Developer, 2026-09-23) · To Do → In Progress (Developer, 2026-09-23) · In Progress → In Review (QA Lead, 2026-09-23)

**Comments (2):**

**QA Lead** (2026-09-23):

QA run 12 against build 4.2.0-rc3: 7 of 9 checks passed. The two failures are reproduced by hand from the recorded steps — the evidence is attached to SAMPLE-105.

**Developer** (2026-09-23):

Fix is on the release branch: the token is invalidated on first use, and a version bump covers the password-change case. Not verified yet — the blocked check also needs the mail catcher enabled in staging.
