# Authentication & Authorization This document describes the authentication and authorization model for the internal delivery note browser. The system uses: - MongoDB to store users (via Mongoose models). - Cookie-based sessions with a signed JWT payload. - Role-aware access control (`branch`, `admin`, `dev`). - Branch-level RBAC enforcement for filesystem-related APIs. --- ## 1. Goals & Scope The main goals of the authentication and authorization system are: - Only authenticated users can access protected backend APIs. - Branch users can only see delivery notes for **their own branch**. - Admin and dev users can access data across branches. - Passwords are never stored in plaintext. - Sessions are stored in signed JWTs in HTTP-only cookies. This document covers: - Environment variables related to auth. - Roles and RBAC rules. - Session payload and cookie configuration. - Auth endpoints (login/logout/me). - Password change (authenticated users). Non-goals (for this document): - Email-based password recovery (documented as a follow-up ticket/phase). --- ## 2. Environment & Configuration ### 2.1 Required variables Auth depends on the following environment variables: - `SESSION_SECRET` (required) - Strong, random string used to sign and verify JWT session tokens. - Minimum length: **32 characters**. - Must be kept secret. - Should differ between environments (dev/staging/prod). Auth endpoints also require DB connectivity: - `MONGODB_URI` (required) ### 2.2 Optional variables - `SESSION_COOKIE_SECURE` (optional) - Overrides the `Secure` cookie flag. - Allowed values: `true` or `false`. Default behavior: - `Secure` cookie is enabled when `NODE_ENV === "production"`. Local HTTP testing (e.g. `http://localhost:3000` with Docker + `next start`): - Set `SESSION_COOKIE_SECURE=false` in your local docker env file. Staging/Production: - Prefer HTTPS. - Keep `SESSION_COOKIE_SECURE` unset (or `true`) and run the app behind TLS. > If the application is served over **plain HTTP** (no TLS), many clients will not send `Secure` cookies back. In that case, logins will appear to “work” (Set-Cookie is present), but subsequent requests will still be unauthenticated. ### 2.3 Fail-fast environment validation The repo provides centralized env validation: - `lib/config/validateEnv.js` validates required env vars and basic sanity checks. - `scripts/validate-env.mjs` runs validation against `process.env`. In Docker, run validation before starting the server: ```sh node scripts/validate-env.mjs && npm run start ``` --- ## 3. Roles ### 3.1 `branch` - Represents a user who belongs to a specific branch/location. - Must have a valid `branchId` (e.g. `"NL01"`). - Intended access pattern: - Can only access delivery notes for their own branch. - Cannot access other branches. ### 3.2 `admin` - Administrator account. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern: - Can access delivery notes across all branches. ### 3.3 `dev` - Development/engineering account. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern: - Full or near-full access. --- ## 4. Authorization: Branch-Level RBAC RBAC is enforced on branch-related filesystem APIs. ### 4.1 Response semantics Error responses use the standardized API error payload: ```json { "error": { "message": "Human readable message", "code": "SOME_MACHINE_CODE", "details": {} } } ``` - **401 Unauthorized**: no valid session (`getSession()` returns `null`). ```json { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } } ``` - **403 Forbidden**: session exists but the user is not allowed to access the requested branch. ```json { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } } ``` ### 4.2 Permission helpers RBAC rules live in `lib/auth/permissions.js`: - `canAccessBranch(session, branchId)` - `filterBranchesForSession(session, branchIds)` ### 4.3 Protected endpoints These endpoints require a valid session: - `GET /api/branches` - `GET /api/branches/[branch]/years` - `GET /api/branches/[branch]/[year]/months` - `GET /api/branches/[branch]/[year]/[month]/days` - `GET /api/files?branch=&year=&month=&day=` - `GET /api/files/:branch/:year/:month/:day/:filename` (binary PDF stream/download) - `GET /api/search` (search is always subject to RBAC) Search RBAC notes: - Branch users can only search within their own branch. - Admin/dev can search across multiple branches. - Query params like `branch=` or `scope=multi&branches=...` are validated and filtered through RBAC. --- ## 5. Sessions & Cookies Sessions are implemented as signed JWTs stored in HTTP-only cookies. ### 5.1 Session payload ```json { "userId": "", "role": "branch | admin | dev", "branchId": "NL01 | null", "iat": 1700000000, "exp": 1700003600 } ``` ### 5.2 JWT signing - Algorithm: `HS256`. - Secret: `SESSION_SECRET`. - Token lifetime: `SESSION_MAX_AGE_SECONDS = 8 hours`. ### 5.3 Cookie settings Cookie name: `auth_session` Attributes: - `httpOnly: true` - `secure: resolved via NODE_ENV + optional SESSION_COOKIE_SECURE override` - `sameSite: "lax"` - `path: "/"` - `maxAge: 8 hours` Implementation lives in `lib/auth/session.js`: - `createSession({ userId, role, branchId })` - `getSession()` - `destroySession()` --- ## 6. Auth Endpoints All endpoints below are implemented as Next.js App Router Route Handlers in `app/api/**/route.js`. ### 6.1 `POST /api/auth/login` Authenticate a user and set the session cookie. Responses: - `200 { "ok": true }` - `400` (invalid JSON/body) ```json { "error": { "message": "Invalid request body", "code": "VALIDATION_INVALID_JSON" } } ``` - `400` (missing username/password) ```json { "error": { "message": "Missing username or password", "code": "VALIDATION_MISSING_FIELD", "details": { "fields": ["username", "password"] } } } ``` - `401` (invalid credentials) ```json { "error": { "message": "Invalid credentials", "code": "AUTH_INVALID_CREDENTIALS" } } ``` - `500` ```json { "error": { "message": "Internal server error", "code": "INTERNAL_SERVER_ERROR" } } ``` ### 6.2 `GET /api/auth/logout` Clears the session cookie. - Returns `200 { "ok": true }` on success. - Logout is idempotent. ### 6.3 `GET /api/auth/me` Return the current session identity for frontend consumers. Rationale: - Frontends should not use `401` as basic control flow to determine “am I logged in?”. - `/api/auth/me` provides a stable, low-friction session check. Response (unauthenticated): ```json { "user": null } ``` Response (authenticated): ```json { "user": { "userId": "...", "role": "branch|admin|dev", "branchId": "NL01" } } ``` Security note: - The endpoint intentionally returns only the minimal session identity. - It does not reveal password hashes or user database internals. --- ## 7. Password Management ### 7.1 Password storage - Passwords are stored as a `bcrypt` hash in `users.passwordHash`. - The API never returns `passwordHash`. ### 7.2 Password policy (current) The password policy is intentionally explicit and testable. Current policy: - Minimum length: **8** characters - Must contain at least **1 letter** (`A–Z`) - Must contain at least **1 number** (`0–9`) - New password must be different from the current password The canonical policy implementation lives in `lib/auth/passwordPolicy.js`. ### 7.3 `POST /api/auth/change-password` (RHL-009) Change the password for the currently authenticated user. Authentication: - Requires a valid session cookie. - If no session exists: `401 AUTH_UNAUTHENTICATED`. Request body (JSON): ```json { "currentPassword": "", "newPassword": "" } ``` Response: - `200 { "ok": true }` Behavior: 1. Validate JSON and required fields. 2. Load the current user by `session.userId`. - If the user cannot be found: treat as invalid session and return `401 AUTH_UNAUTHENTICATED`. 3. Verify that `currentPassword` matches `passwordHash`. - If mismatch: return `401 AUTH_INVALID_CREDENTIALS`. 4. Validate `newPassword` against the password policy. - If weak: return `400 VALIDATION_WEAK_PASSWORD` with structured `details`. 5. Hash and persist the new password. 6. Clear flags / sensitive reset state: - `mustChangePassword = false` - `passwordResetToken = null` - `passwordResetExpiresAt = null` Error responses: - `400 VALIDATION_INVALID_JSON` - `400 VALIDATION_INVALID_BODY` - `400 VALIDATION_MISSING_FIELD` (details: `{ fields: [ ... ] }`) - `400 VALIDATION_WEAK_PASSWORD` - `401 AUTH_UNAUTHENTICATED` - `401 AUTH_INVALID_CREDENTIALS` - `500 INTERNAL_SERVER_ERROR` Example: weak password ```json { "error": { "message": "Weak password", "code": "VALIDATION_WEAK_PASSWORD", "details": { "minLength": 8, "requireLetter": true, "requireNumber": true, "disallowSameAsCurrent": true, "reasons": ["MIN_LENGTH", "MISSING_NUMBER"] } } } ``` ### 7.4 Password reset (planned, separate ticket) The user model includes fields that allow implementing password reset flows: - `mustChangePassword` - `passwordResetToken` - `passwordResetExpiresAt` However, the email/token based recovery flow is intentionally implemented as a **separate follow-up ticket** (Phase B), because it introduces additional security surface (token handling and non-leakage) and external SMTP/IT dependencies. --- ## 8. Security Notes - Use HTTPS for real users (staging/prod). - Keep `SESSION_SECRET` secret and rotate when needed. - Local HTTP testing is supported via `SESSION_COOKIE_SECURE=false`. - Password-changing endpoints should avoid leaking sensitive information: - `AUTH_INVALID_CREDENTIALS` is used for invalid current password. - Weak-password responses should return only safe, structured details (policy + reasons).