Files
nick-doc/04 - Flows/Authentication Flow.md
Siavash Sameni dceaf82934 audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs:
- Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md
- 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer).
- Scanner docs from scratch (was zero): architecture, data model, API ref, payment
  flow, operations runbook + repo README.
- Doc-sync updates across API reference, data models, flows, design system.
- Secret Rotation Runbook (08 - Operations) for the exposed credentials.
- Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js.

Issues remain status:open intentionally — the code fixes are uncommitted-then-committed
working-tree changes per repo and aren't "resolved" until merged/deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:48:04 +04:00

16 KiB
Raw Permalink Blame History

title, tags, related_models, related_apis
title tags related_models related_apis
Authentication Flow
flow
auth
jwt
login
security
User
TempVerification
Auth API
POST /api/auth/login
POST /api/auth/refresh-token
POST /api/auth/logout

Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)

[!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.

Authentication Flow

End-to-end specification for email + password authentication, JWT issuance, token lifecycle, refresh-token rotation, and logout cleanup. This is the most-used auth path in the marketplace and underpins every protected API call and Socket.IO room subscription.

Actors

  • User (Buyer / Seller / Admin) submits credentials via the frontend.
  • Frontend (Next.js) the JWT sign-in view in frontend/src/auth/view/jwt/jwt-sign-in-view.tsx, which delegates to signInWithPassword() in frontend/src/auth/context/jwt/action.ts.
  • Backend (Express) AuthController.login in backend/src/services/auth/authController.ts.
  • MongoDB User collection (refresh tokens are appended to user.refreshTokens[]).
  • Redis session store (sessionService.createSession) and login-attempt rate-limiter (rateLimitService.checkLoginAttempts).
  • Socket.IO not directly involved in login, but the issued JWT is later used to join user-${userId} rooms (see Notification Flow and Chat Flow).

Preconditions

  • The user has already completed Registration Flow and their User.isEmailVerified === true.
  • The user's User.status === "active" (soft-deleted accounts have status "deleted").
  • Network connectivity is available (the frontend uses NetworkUtils.isOnline() from frontend/src/auth/utils/error-handler.ts).
  • localStorage is available — the frontend rejects the request early via StorageUtils.isAvailable() if storage is blocked.
  • Backend env vars JWT_SECRET, JWT_EXPIRES_IN, REFRESH_TOKEN_EXPIRES_IN are set (backend/src/services/auth/authService.ts:19-21).

Step-by-step narrative

  1. User fills the sign-in form at /auth/jwt/sign-in and clicks "Sign in". The form is implemented in frontend/src/auth/view/jwt/jwt-sign-in-view.tsx.
  2. Client-side guards: signInWithPassword() (action.ts:32-116) verifies the browser is online and localStorage is writable; otherwise it throws a typed AuthErrorHandler error.
  3. HTTP request: The frontend POSTs { email, password } to POST /api/auth/login (resolved by endpoints.auth.login in frontend/src/lib/axios.ts). An AbortController is armed with a 60-second timeout.
  4. Validation middleware runs loginValidation (backend/src/services/auth/authValidation.ts) — wires into Express via authRoutes.ts:22.
  5. Cloudflare Turnstile CAPTCHA gate (captchaGate middleware, commit b8edbbf): Before the rate-limiter runs, captchaGate checks the in-memory failure counter for the caller's IP. If that IP has accumulated 3 or more failed login attempts within 15 minutes, a valid cf-turnstile-response token must be present in the request body. Without it the endpoint returns 429 { captchaRequired: true }. If TURNSTILE_SECRET_KEY is not set (local dev), the gate is skipped. On CAPTCHA pass, the middleware calls Cloudflare's siteverify endpoint to validate the token before proceeding. 5a. Rate limiting: rateLimitService.checkLoginAttempts(email, 5, 15*60) is called (authController.ts:173). The counter is incremented on every login attempt (before password comparison), not only on failures. Once 5 total attempts accumulate within a 15-minute window, the endpoint returns 429 TOO_MANY_ATTEMPTS. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts.
  6. User lookup: User.findOne({ email, status: "active" }).select("+password")password is select: false by default in the schema and must be explicitly projected.
  7. Password comparison: authService.comparePassword() invokes bcrypt.compare() (cost factor 12 — see authService.ts:102-105). Constant-time per bcrypt's design.
  8. Email-verification gate: If !user.isEmailVerified, returns 403 EMAIL_NOT_VERIFIED with needsVerification: true. The frontend intercepts this in action.ts:104-111 and redirects to /auth/jwt/verify?email=....
  9. Reset attempts: On success, rateLimitService.resetLoginAttempts(email) wipes the Redis counter.
  10. Last-login stamp: user.lastLoginAt = new Date() is saved.
  11. Token issuance:
    • Access tokenauthService.generateToken() builds a JWT with { id, email, role, isEmailVerified, iat }, signed with HS256, issuer: 'marketplace-backend', audience: 'marketplace-users', default expiresIn from config.
    • Refresh tokenauthService.generateRefreshToken() issues a separate JWT with { id, type: 'refresh' } and a longer TTL.
  12. Refresh-token persistence: The new refresh token is appended to the User.refreshTokens array (authController.ts:230-231). This array is the server-side allow-list — only tokens present here can be used to mint new access tokens.
  13. Redis session: sessionService.createSession(accessToken, userId, email, role, ip, userAgent, 86400) stores a 24h session record keyed by the access token (authController.ts:235-247). Failures are logged but do not block login.
  14. Response: 200 OK with { user: user.toJSON(), tokens: { accessToken, refreshToken } }. toJSON() strips password, refreshTokens, and verification codes (see User model toJSON transform).
  15. Client-side storage: The frontend writes both tokens to localStorage via StorageUtils.safeSet() — keys accessToken and refreshToken (action.ts:77-82).

[!warning] Token storage is localStorage, not cookies Tokens are persisted in window.localStorage. This is intentional (the API is a separate origin and the app is fully SPA-like), but it means tokens are reachable from any script running on the page. Mitigations in place: strict CSP via helmet, no third-party scripts in the auth views, and short access-token TTL with refresh rotation. There are no httpOnly auth cookies.

  1. Axios interceptor (frontend/src/lib/axios.ts) attaches Authorization: Bearer ${accessToken} to every subsequent request. On a 401 response, the interceptor automatically triggers the refresh flow described below. A 403 response (e.g., EMAIL_NOT_VERIFIED) is not retried via refresh — it is surfaced directly to the caller. The interceptor only checks status === 401 (axios.ts:105); 403 responses are not handled by the interceptor and propagate as errors.
  2. Socket.IO bootstrap: After login, the dashboard layout connects to Socket.IO and emits join-user-room, join-buyer-room/join-seller-room based on user.role. See backend/src/app.ts:83-126.

Sequence diagram

sequenceDiagram
    autonumber
    actor U as User
    participant FE as Frontend (Next.js)
    participant BE as Backend (Express)
    participant DB as MongoDB
    participant R as Redis
    participant IO as Socket.IO

    U->>FE: Enter email + password, click Sign in
    FE->>FE: NetworkUtils.isOnline() / StorageUtils.isAvailable()
    FE->>BE: POST /api/auth/login { email, password }
    BE->>R: rateLimitService.checkLoginAttempts(email)
    R-->>BE: { allowed: true, remaining }
    BE->>DB: User.findOne({ email, status: "active" }).select("+password")
    DB-->>BE: user document
    BE->>BE: bcrypt.compare(password, user.password)
    alt password invalid
        BE-->>FE: 401 Invalid credentials
    else email not verified
        BE-->>FE: 403 EMAIL_NOT_VERIFIED
        FE-->>U: Redirect /auth/jwt/verify
    else success
        BE->>R: rateLimitService.resetLoginAttempts(email)
        BE->>DB: set user.lastLoginAt = now
        BE->>DB: save new refresh token
        BE->>BE: generateToken(authUser) / generateRefreshToken(authUser)
        BE->>R: sessionService.createSession(accessToken, ...)
        BE-->>FE: 200 { user, tokens: { accessToken, refreshToken } }
        FE->>FE: localStorage.setItem('accessToken' / 'refreshToken')
        FE->>IO: socket.emit('join-user-room', userId)
        FE-->>U: Redirect to /dashboard
    end

API calls

Method Endpoint Source
POST /api/auth/login backend/src/services/auth/authRoutes.ts:22authController.login
POST /api/auth/telegram authRoutes.tsauthController.telegramAuth
POST /api/auth/refresh-token authRoutes.ts:24-27authController.refreshToken
POST /api/auth/logout authRoutes.ts:68authController.logout (protected)
GET /api/auth/profile authRoutes.ts:69authController.getProfile
DELETE /api/auth/account authRoutes.ts:86-89authController.deleteAccount (requires password in body, runs deleteAccountValidation)

Telegram first-class auth flow

Telegram is now a peer auth provider alongside email/password, Google, and passkeys.

  1. The Telegram Mini App shell reads raw signed launch data from window.Telegram.WebApp.initData; browser login can submit a Telegram Login Widget payload.
  2. The frontend posts the raw signed payload to POST /api/auth/telegram; it never trusts initDataUnsafe for authentication.
  3. Backend verifies the Telegram signature:
    • Mini App uses verifyMiniAppInitData() with the Telegram WebAppData HMAC algorithm.
    • Login Widget uses verifyTelegramLoginWidget() with the Telegram Login Widget HMAC algorithm.
  4. Backend rejects stale auth_date, bot accounts, replayed Mini App initData, and blocked TelegramLink records.
  5. If an active TelegramLink exists, backend signs in that Amanat user. If no link exists, backend creates a Telegram-only user with nullable email, authProvider: "telegram", telegramVerified: true, then creates the link.
  6. Backend issues the standard access token and refresh token pair. The frontend stores them in localStorage under accessToken and refreshToken, then hydrates the current session.
  7. If isNewUser=true, the Mini App opens a lightweight onboarding overlay and points the user to account settings for optional email, language, currency, and wallet details.

High-risk actions are unchanged: escrow release, refund, dispute-sensitive, and wallet-sensitive operations still use the existing protected backend authorization and step-up gates. Telegram auth only establishes the user session.

Passkey auth flow

The frontend registerPasskey and authenticateWithPasskey actions call passkey API endpoints. All passkey API calls are proxied directly to the Express backend via the next.config.ts rewrite rule (/api/:path* → backend). There are no Next.js route handler files (route.ts) for passkey paths — requests travel: browser → Next.js dev server (rewrite) → Express backend.

Database writes

  • users collection: lastLoginAt updated; refreshTokens array gains one entry per successful login or refresh.
  • No write at all on a failed attempt (only Redis counter increments).
  • On changePassword / resetPassword: refreshTokens is reset to [] (forces re-login on every device — authController.ts:600 and :685).

Socket events emitted

  • Login itself emits nothing. After the response, the frontend emits the client-side events join-user-room, join-buyer-room or join-seller-room to subscribe to targeted notifications.

Side effects

  • Redis session: 24-hour key holding { userId, email, role, ip, userAgent }. Used for forced logout (admin can call sessionService.deleteSession(token)).
  • Redis rate-limit counter: TTL 15 min, reset on success. Counter increments on every attempt regardless of outcome.
  • No email is sent on a normal login (no "new sign-in" notification today — opportunity for future enhancement).
  • Sentry: any unexpected exception bubbles to Sentry.setupExpressErrorHandler (app.ts:351).

Refresh-token flow

The access token is short-lived. When a protected request returns 401 TOKEN_INVALID, the axios interceptor calls:

  1. POST /api/auth/refresh-token with { refreshToken } from localStorage.
  2. Backend authController.refreshToken (:263-313) verifies the token via verifyRefreshToken, checks it is still present in user.refreshTokens[], then issues a brand-new access and refresh token.
  3. The old refresh token is removed from the array and the new one is pushed — implementing refresh-token rotation. A leaked-but-stale token therefore becomes invalid the moment the legitimate user refreshes.
  4. The new pair is written back to localStorage and the original failed request is retried.

[!note] 403 responses are not retried The interceptor only triggers token refresh for status === 401 (axios.ts:105). A 403 (e.g., EMAIL_NOT_VERIFIED) is passed through directly to the caller without attempting a refresh.

[!warning] Refresh-token sequence diagram is truncated The Mermaid diagram below is incomplete — it was truncated in the original source at the point where the backend checks that the refresh token exists in user.refreshTokens. The remaining steps (rotate tokens, persist, respond, retry original request) are described in prose above but are not yet rendered in the diagram.

sequenceDiagram
    autonumber
    participant FE as Frontend axios
    participant BE as Backend
    participant DB as MongoDB

    FE->>BE: GET /api/marketplace/... (Bearer access)
    BE-->>FE: 401 TOKEN_INVALID
    FE->>BE: POST /api/auth/refresh-token { refreshToken }
    BE->>BE: verifyRefreshToken(refreshToken)
    BE->>DB: User.findById(decoded.id)
    BE->>DB: ensure refresh token is in user.refreshTokens
    Note over BE,DB: (diagram truncated — remaining steps documented in prose above)

Account management

changePassword (API-only)

POST /api/auth/change-password exists on the backend and the changePassword() action is defined in frontend/src/auth/context/jwt/action.ts. However:

[!warning] No frontend UI for change-password There is no dashboard page that renders a change-password form. The feature is API-only at this time. Users cannot change their password through the UI; a developer or direct API client must call the endpoint manually.

deleteAccount

[!bug] Account deletion frontend calls wrong endpoint The frontend deleteAccount action calls DELETE /user/profile, which does not exist on the backend. The real backend endpoint is DELETE /api/auth/account (authRoutes.ts:86-89), which requires a password field in the request body and runs deleteAccountValidation middleware. Until the frontend is updated to call the correct endpoint, account deletion will always fail with a 404 or routing error.

Known issues summary

Issue Severity Details
deleteAccount calls wrong endpoint Bug Frontend calls DELETE /user/profile; correct backend endpoint is DELETE /api/auth/account (requires password in body)
No change-password UI Gap POST /api/auth/change-password and changePassword() action exist but no dashboard page renders the form
Rate limiter counts all attempts Clarification Counter increments before password check — 5 total attempts (not 5 failures) triggers lockout
Axios interceptor 401-only Clarification Interceptor only auto-refreshes on status === 401 (axios.ts:105); 403 errors propagate directly to caller
Refresh-token diagram truncated Doc debt Mermaid diagram cut off mid-flow; prose description is authoritative

Linked flows

Source files

  • Backend: backend/src/services/auth/authController.ts
  • Backend: backend/src/services/auth/authService.ts
  • Backend: backend/src/services/auth/authValidation.ts
  • Backend: backend/src/services/auth/authRoutes.ts
  • Frontend: frontend/src/auth/view/jwt/jwt-sign-in-view.tsx
  • Frontend: frontend/src/auth/context/jwt/action.ts
  • Frontend: frontend/src/lib/axios.ts