Files
nick-doc/04 - Flows/Password Reset Flow.md
Siavash Sameni a1f056e6a5 docs: align flow docs with code reality + create 35 implementation issue files
Flow docs updated (11 files):
- Delivery Confirmation: reversed actor roles (buyer generates, seller verifies),
  fixed endpoint paths (/delivery-code/generate, /delivery-code/verify)
- Passkey (WebAuthn): removed stub/simulated-key claims; real @simplewebauthn/server
  attestation is implemented; refresh tokens are persisted
- Dispute: corrected resolve schema (action enum), removed non-existent statuses,
  documented security gaps (no role guards on status/resolve/assign), route shadowing,
  all socket events are TODO stubs
- Seller Offer: corrected all endpoint paths, removed 'active' status, documented
  withdraw dead code, missing seller history page, select-offer notification gap
- Notification: corrected mark-all-read method+path, fixed GET /:id broken lookup,
  added unread-count-update socket event
- Authentication: corrected rate limiter (counts all attempts), axios 403 not handled,
  deleteAccount wrong endpoint bug, changePassword no UI
- Password Reset: corrected 6-digit code (not 8), documented no-complexity gap on
  reset-with-code vs token reset
- Payment Flow DePay: /create→/save, removed phantom sub-routes, SIM_ bypass risk,
  PaymentProvider type gap, getProviderIntentEndpoint routing bug
- Payment Flow SHKeeper: removed phantom polling endpoint, fixed release/refund paths
- Purchase Request: added pending_payment/active statuses, fixed sellers/attachments
  endpoints, corrected socket events, PUT→PATCH bug
- Escrow: documented dispute resolve does not touch escrow, route shadowing, confirm-delivery auth gap

Issues created (35 files in Issues/):
- 9 security issues (critical) including: dispute privilege escalation ×4,
  unauthenticated payment/scanner endpoints ×2, SIM_ production bypass,
  confirm-delivery ownership gap
- 26 additional major/critical bugs covering broken endpoints, missing features,
  data integrity gaps, and frontend-backend mismatches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:47:49 +04:00

162 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Password Reset Flow
tags: [flow, auth, password-reset, email]
related_models: ["[[User]]"]
related_apis: ["POST /api/auth/request-password-reset", "POST /api/auth/reset-password-with-code", "POST /api/auth/reset-password"]
---
> [!caution] Audit note — last reviewed 2026-05-29
> Several discrepancies between frontend code, backend code, and this document were identified and corrected in this revision. See inline ⚠️ markers for known bugs and mismatches.
# Password Reset Flow
Self-service password recovery. There are **two separate reset endpoints** with different security characteristics:
| Endpoint | Mechanism | Password complexity enforced? |
|---|---|---|
| `POST /api/auth/reset-password-with-code` | 6-digit emailed code | **No** — no validation middleware |
| `POST /api/auth/reset-password` | Token-based (link in email) | **Yes**`passwordResetValidation` requires uppercase + lowercase + digit |
The primary UI-driven path uses the **code-based** endpoint. The token-based endpoint is a legacy/alternative variant.
## Actors
- **User** who has forgotten their password.
- **Frontend** — `frontend/src/auth/view/jwt/jwt-reset-password-view.tsx` (request) and `jwt-update-password-view.tsx` (submit new password).
- **Backend** — `AuthController.requestPasswordReset` and `AuthController.resetPasswordWithCode` in `backend/src/services/auth/authController.ts`.
- **MongoDB** — `User` collection (`passwordResetCode`, `passwordResetCodeExpires`, `refreshTokens`).
- **Email service** — `emailService.sendPasswordResetCodeEmail`.
## Preconditions
- The account exists and `status === "active"` (deleted accounts are silently treated as non-existent).
- The user has access to the email inbox associated with the account.
- A 6-digit code is valid for **1 hour** (`authController.ts:556`).
## Step-by-step narrative
1. User clicks "Forgot password?" on the sign-in page and lands at `/auth/jwt/reset-password`.
2. User enters their email and submits.
3. Frontend POSTs `POST /api/auth/request-password-reset { email }`.
4. Backend `authController.requestPasswordReset` (`:542-574`):
- `User.findOne({ email, status: "active" })`. If absent, returns `200` with the same generic message — **no enumeration**.
- Generates a **6-digit** code via `authService.generateVerificationCode()` (`Math.floor(100000 + Math.random() * 900000)`).
- Saves `passwordResetCode` and `passwordResetCodeExpires = now + 3_600_000 ms` on the user.
- Calls `emailService.sendPasswordResetCodeEmail(email, firstName, code)`.
5. Response: `200 "If an account with this email exists, a password reset code has been sent"` regardless of outcome.
6. User receives the email and enters the code + new password on `/auth/jwt/update-password`.
7. Frontend POSTs `POST /api/auth/reset-password-with-code { email, code, password }`.
8. Backend `authController.resetPasswordWithCode` (`:611-657`):
- Validates code format `/^\d{6}$/` — a code of any other length (e.g., 8 digits) will **always fail** here.
- `User.findOne({ email, passwordResetCode: code, passwordResetCodeExpires: { $gt: now }, status: "active" })`. Mismatch → `400 Invalid or expired reset code`.
- Hashes the new password with bcrypt cost 12. **No password complexity validation is applied** — weak passwords such as `123456` or `aaaaaa` are accepted without error.
- Sets `user.password = hashed`, clears `passwordResetCode` and `passwordResetCodeExpires`, **wipes `user.refreshTokens = []`** to invalidate all existing sessions.
- Saves.
9. Response: `200 "Password reset successfully"`. Frontend redirects to `/auth/jwt/sign-in` for a fresh login.
## Sequence diagram
```mermaid
sequenceDiagram
autonumber
actor U as User
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
participant MAIL as Email Service
U->>FE: Click "Forgot password", enter email
FE->>BE: POST /api/auth/request-password-reset { email }
BE->>DB: User.findOne({ email, status: "active" })
alt user found
BE->>BE: code = generateVerificationCode() [6 digits]
BE->>DB: user.passwordResetCode = code\nexpires = +1h
BE->>MAIL: sendPasswordResetCodeEmail(email, firstName, code)
MAIL-->>U: Email with 6-digit code
end
BE-->>FE: 200 "if account exists, code sent"
U->>FE: Enter code + new password
FE->>BE: POST /api/auth/reset-password-with-code { email, code, password }
BE->>BE: isValidVerificationCode(code) [/^\d{6}$/]
BE->>DB: User.findOne({ email, code, expires>now })
BE->>BE: bcrypt.hash(password, 12) [no complexity check]
BE->>DB: user.password = hash\nuser.refreshTokens = []\nclear reset fields
BE-->>FE: 200 "Password reset successfully"
FE-->>U: Redirect /auth/jwt/sign-in
```
## API calls
| Method | Endpoint | Source | Notes |
|---|---|---|---|
| `POST` | `/api/auth/request-password-reset` | `authRoutes.ts:44-47` | Sends 6-digit code by email |
| `POST` | `/api/auth/reset-password-with-code` | `authRoutes.ts:54-56` | Code-based; **no complexity validation** |
| `POST` | `/api/auth/reset-password` | `authRoutes.ts:49-52` | Token-based variant; enforces complexity via `passwordResetValidation` |
## Two-endpoint comparison
> [!important] Code-based vs token-based reset endpoints
>
> **`POST /api/auth/reset-password-with-code`** (primary UI path)
> - Uses a 6-digit numeric code delivered by email.
> - `isValidVerificationCode()` validates with `/^\d{6}$/`. An 8-digit code will always fail.
> - Has **no password complexity middleware**. Any string is accepted as the new password.
>
> **`POST /api/auth/reset-password`** (legacy token-based path)
> - Uses a URL token (link in email) rather than a short code.
> - Enforces password complexity via `passwordResetValidation` middleware (requires uppercase, lowercase, and a digit).
>
> The two endpoints provide inconsistent security guarantees. Users who reset via the code flow can set a weak password that would be rejected by the token flow.
## Database writes
- **`users` collection**: on request, sets `passwordResetCode` + `passwordResetCodeExpires`. On submit, replaces `password`, clears reset fields, and empties `refreshTokens`.
## Socket events emitted
- None.
## Side effects
- **Email**: one transactional message containing the 6-digit code.
- **Server-side log**: `authController.ts:559` `console.log` includes the generated code in plain text — same hardening note as [[Registration Flow]].
- **Session invalidation**: All refresh tokens cleared → all devices forced to re-login after password change. Access tokens still valid until expiry (typically minutes).
## Error / edge cases
- **Unknown email** → always `200`, generic message. No enumeration.
- **Invalid code format** → `400` from `isValidVerificationCode` guard before DB lookup. Note: the `authController.ts` comment mentions "8 digits" but the actual implementation generates and validates exactly 6 digits — any 8-digit code will be rejected.
- **Expired code** (>1h) → `400 Invalid or expired reset code`.
- **Multiple parallel requests** → each overwrites the previous `passwordResetCode`; the latest email wins, prior codes silently invalidated.
- **User attempts reset on deleted account** → treated as unknown (no email sent, `200` returned).
- **Email delivery failure** → response still `200`; user can request again.
- **Access tokens still valid post-reset** → unavoidable with stateless JWT; mitigated by short TTL. Critical operations should re-verify password.
> [!warning] Plaintext code in logs
> Same as [[Registration Flow]]: the reset code is `console.log`-ed by the controller in all environments. Restrict log access in production or gate the log behind `NODE_ENV !== 'production'`.
> [!bug] Controller comment says "8 digits" but code generates 6
> The comment in `authController.ts` describes an 8-digit code, but `authService.generateVerificationCode()` uses `Math.floor(100000 + Math.random() * 900000)`, which produces a number in the range 100000999999 (exactly 6 digits). `isValidVerificationCode()` enforces `/^\d{6}$/`. Any 8-digit value sent to `reset-password-with-code` will always be rejected. The comment is wrong; the 6-digit implementation and validation are correct and consistent.
## Known issues summary
| Issue | Severity | Details |
|---|---|---|
| No password complexity on code-based reset | Security gap | `POST /api/auth/reset-password-with-code` has no complexity middleware; weak passwords accepted |
| Controller comment says 8 digits | Doc bug | Comment is wrong; code generates and validates exactly 6 digits |
| Inconsistent complexity between reset endpoints | Security gap | Token-based reset enforces complexity; code-based reset does not |
## Linked flows
- [[Authentication Flow]] — user re-signs-in after reset.
- [[Registration Flow]] — same code-generation utility.
## Source files
- Backend: `backend/src/services/auth/authController.ts:542-657`
- Backend: `backend/src/services/email/emailService.ts` (`sendPasswordResetCodeEmail`)
- Frontend: `frontend/src/auth/view/jwt/jwt-reset-password-view.tsx`
- Frontend: `frontend/src/auth/view/jwt/jwt-update-password-view.tsx`
- Frontend: `frontend/src/auth/context/jwt/action.ts:181-200`, `:261-276`