# 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`, `superadmin`, `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-like 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. - Role model and permission semantics. - 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). - User management endpoints (planned as **RHL-012**; this doc only defines the required permission semantics). --- ## 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 & Capabilities (RHL-041) ### 3.1 Role enum The role enum is: - `branch | admin | superadmin | dev` ### 3.2 Capability separation This project intentionally separates **two independent capabilities**: 1. **Branch access** (existing capability) - Governs whether a user can browse/search/open PDFs for a given branch. 2. **User management** (new capability, used by RHL-012) - Governs whether a user can create/update/manage user accounts. ### 3.3 Role matrix | Role | Branch access | User management | | ------------ | ------------------- | --------------- | | `branch` | Only own `branchId` | No | | `admin` | All branches | No | | `superadmin` | All branches | Yes | | `dev` | All branches | Yes | ### 3.4 Role descriptions #### 3.4.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.4.2 `admin` - Administrator account. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern: - Can access delivery notes across all branches. - **Cannot** manage users. #### 3.4.3 `superadmin` - Administrator account with user-management permission. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern: - Can access delivery notes across all branches. - **Can** manage users. #### 3.4.4 `dev` - Development/engineering account. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern: - Can access delivery notes across all branches. - **Can** manage users. ### 3.5 Backward compatibility - Existing `admin` users keep “access all branches”. - Existing `dev` users keep “manage users”. - `superadmin` is only granted to explicitly designated accounts. --- ## 4. Authorization ### 4.1 Branch access (RBAC) Branch access is enforced on filesystem-related endpoints. Rules: - **401 Unauthorized**: no valid session (`getSession()` returns `null`). - **403 Forbidden**: session exists but branch access is not allowed. Standard error payload: ```json { "error": { "message": "Human readable message", "code": "SOME_MACHINE_CODE", "details": {} } } ``` Common branch RBAC responses: - `401 AUTH_UNAUTHENTICATED` ```json { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } } ``` - `403 AUTH_FORBIDDEN_BRANCH` ```json { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } } ``` ### 4.2 User management capability (RHL-012 prerequisite) User management is a separate permission capability from branch access. Rules: - Allowed: `superadmin`, `dev` - Forbidden: `admin`, `branch` For endpoints guarded by this capability, the standardized error is: - `403 AUTH_FORBIDDEN_USER_MANAGEMENT` ```json { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_USER_MANAGEMENT" } } ``` ### 4.3 Permission helpers Authorization rules live in `lib/auth/permissions.js`: - `canAccessBranch(session, branchId)` - `filterBranchesForSession(session, branchIds)` - `canManageUsers(session)` - `requireUserManagement(session)` --- ## 5. Sessions & Cookies Sessions are implemented as signed JWTs stored in HTTP-only cookies. ### 5.1 Session payload ```json { "userId": "", "role": "branch | admin | superadmin | dev", "branchId": "NL01 | null", "email": "name@company.tld | null", "iat": 1700000000, "exp": 1700003600 } ``` Notes: - `email` is optional and may be `null`. - The session email is used for displaying read-only account information in the UI (Profile). ### 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, email })` - `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. Response (unauthenticated): ```json { "user": null } ``` Response (authenticated): ```json { "user": { "userId": "...", "role": "branch|admin|superadmin|dev", "branchId": "NL01", "email": "nl01@example.com" } } ``` Notes: - `email` is optional and may be `null`. - The endpoint intentionally returns only minimal identity information needed by the UI. --- ## 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` ### 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**, 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`. - Backend RBAC is authoritative; UI RBAC exists only for user experience. - User management endpoints (RHL-012) must be guarded using `requireUserManagement(session)`.