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

9.1 KiB
Raw Blame History

title, tags, related_models, related_apis
title tags related_models related_apis
Password Reset Flow
flow
auth
password-reset
email
User
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) YespasswordResetValidation 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.
  • Frontendfrontend/src/auth/view/jwt/jwt-reset-password-view.tsx (request) and jwt-update-password-view.tsx (submit new password).
  • BackendAuthController.requestPasswordReset and AuthController.resetPasswordWithCode in backend/src/services/auth/authController.ts.
  • MongoDBUser collection (passwordResetCode, passwordResetCodeExpires, refreshTokens).
  • Email serviceemailService.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

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 format400 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

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