--- title: Authentication Flow tags: [flow, auth, jwt, login, security] related_models: ["[[User]]", "[[TempVerification]]"] related_apis: ["[[Auth API]]", "POST /api/auth/login", "POST /api/auth/refresh-token", "POST /api/auth/logout"] --- # 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. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). Five failures within 15 minutes returns `429 TOO_MANY_ATTEMPTS`. 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 token** — `authService.generateToken()` builds a JWT with `{ id, email, role, isEmailVerified, iat }`, signed with `HS256`, `issuer: 'marketplace-backend'`, `audience: 'marketplace-users'`, default `expiresIn` from config. - **Refresh token** — `authService.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**. 16. **Axios interceptor** (`frontend/src/lib/axios.ts`) attaches `Authorization: Bearer ${accessToken}` to every subsequent request and, on `401/403`, automatically calls the refresh flow described below. 17. **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 ```mermaid 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:22` → `authController.login` | | `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27` → `authController.refreshToken` | | `POST` | `/api/auth/logout` | `authRoutes.ts:68` → `authController.logout` (protected) | | `GET` | `/api/auth/profile` | `authRoutes.ts:69` → `authController.getProfile` | ## 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. - **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` or `403`, 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. ```mermaid 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