Perfekt, dann machen wir jetzt einmal eine **saubere, komplette `docs/auth.md`** mit allem, was wir bisher besprochen haben – inklusive des geplanten Epics **“Password management & recovery”**. Alles in Englisch, strukturiert, so dass du es 1:1 als Datei ablegen kannst. --- # Authentication & Authorization This document describes the authentication and authorization model for the internal delivery note browser. The system uses: - MongoDB to store users. - Cookie-based sessions with a signed JWT payload. - Role-aware access control (`branch`, `admin`, `dev`). - Future extensions for password management and recovery. > NOTE: This document is a living document. As we extend the auth system (sessions, routes, policies, password flows), we will update this file. --- ## 1. Goals & Scope The main goals of the authentication system are: - Only authenticated users can access the application. - 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 as signed JWTs in HTTP-only cookies. - The system is ready for password change and password recovery functionality. This document covers: - User model and roles. - Session payload and cookie configuration. - Login and logout endpoints. - Planned endpoints for password management and recovery. - Security considerations and implementation guidelines. --- ## 2. User Model Users are stored in MongoDB using the `User` collection. ### 2.1 Fields - **username** (`String`, required, unique, lowercased) - Human-chosen login name. - Stored in lowercase to enable case-insensitive login. - Trimmed, minimum length 3 characters. - Unique index to enforce one user per username. - **email** (`String`, required, unique, lowercased) - Contact address used for password recovery and notifications. - Typically the branch email address for branch accounts, or a personal email address for individual users. - Stored in lowercase. - Unique per user. - **passwordHash** (`String`, required) - Hashed password (e.g. using bcrypt). - Plaintext passwords are never stored. - Always excluded from JSON serialization. - **role** (`String`, required, enum: `"branch" | "admin" | "dev"`) - Controls the type of access a user has. - See **Roles** section below. - **branchId** (`String | null`) - Identifies the branch (e.g. `"NL01"`) that the user belongs to. - Required for `role = "branch"`. - Must be `null` or unused for non-branch users (`admin`, `dev`). - **mustChangePassword** (`Boolean`, default: `false`) - When `true`, the user should be forced to set a new password on the next login. - Useful for first-time login or admin-enforced password resets. - **passwordResetToken** (`String | null`) - Token used for password reset flows. - Generated and validated by the backend. - Not exposed via public APIs. - May be `null` if there is no active reset request. - **passwordResetExpiresAt** (`Date | null`) - Expiry timestamp for the `passwordResetToken`. - Used to ensure that reset links are only valid for a limited time. - May be `null` if there is no active reset request. - **createdAt** (`Date`, auto-generated) - Timestamp when the user record was created. - **updatedAt** (`Date`, auto-generated) - Timestamp when the user record was last updated. ### 2.2 Validation Rules & Invariants - `username` must be unique and is stored in lowercase. - `email` must be unique and is stored in lowercase. - `passwordHash` must be present for all users. - When `role = "branch"`, `branchId` must be a non-empty string. - For `role = "admin"` and `role = "dev"`, `branchId` is optional and usually `null`. - `passwordResetToken` and `passwordResetExpiresAt` must be consistent: - If one is set, the other should also be set. - Once a reset is completed or expired, both should be cleared. ### 2.3 Serialization Rules When converting `User` documents to JSON or plain objects (e.g. in API responses), the following fields must be hidden: - `passwordHash` - `passwordResetToken` This ensures that sensitive information is not exposed via API responses or logs. --- ## 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 (high-level): - Can only access delivery notes for their own branch. - Cannot access other branches. - No global configuration or system-wide administration. ### 3.2 `admin` - System administrator. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern (high-level): - Can access delivery notes across all branches. - Can perform user administration (create/update users). - Can perform configuration-level changes. ### 3.3 `dev` - Development/engineering account. - Used for debugging, maintenance, and operational tooling. - Typically not bound to any single branch (`branchId = null`). - Intended access pattern (high-level): - Full or near-full access to the system. - Can be used in development/staging environments. - Production use should be limited and auditable. ### 3.4 Role Assignment & User Provisioning - Users are **created by an admin** (no public self-registration). - When a user is created: - `role` is set by the admin. - `branchId` is set by the admin and cannot be chosen by the user. - For branch accounts, we typically create one or more users per branch with: - `role = "branch"` - `branchId` set to the respective branch identifier. --- ## 4. Sessions & Cookies Sessions are implemented as signed JWTs stored in HTTP-only cookies. ### 4.1 Session Payload Format A session payload has the following structure: ```json { "userId": "", "role": "branch | admin | dev", "branchId": "NL01 | null", "iat": 1700000000, "exp": 1700003600 } ``` - `userId` (string): MongoDB `_id` of the user. - `role` (string): One of `"branch"`, `"admin"`, `"dev"`. - `branchId` (string or `null`): Branch identifier for branch users, or `null` for admin/dev users. - `iat` (number): Issued-at timestamp (UNIX time). - `exp` (number): Expiration timestamp (UNIX time). The `iat` and `exp` fields are managed by the JWT library. ### 4.2 JWT Signing - The JWT is signed using a symmetric secret (`SESSION_SECRET`). - Recommended algorithm: `HS256` (HMAC using SHA-256). - The secret is defined via environment variable: - `SESSION_SECRET` (required, strong random string) - Token lifetime (example): - Access token / session lifetime: e.g. 8 hours (configurable). ### 4.3 Cookie Settings The session token is stored in an HTTP-only cookie, for example: - **Cookie name**: `auth_session` (TBD, but must be consistent across backend/frontend) - **Attributes**: - `httpOnly: true` - `secure: process.env.NODE_ENV === "production"` - `sameSite: "lax"` (or stricter, e.g. `"strict"` if acceptable) - `path: "/"` (cookie is sent for all paths) - `maxAge`: matches or slightly exceeds the JWT `exp` lifetime. Cookies are written and cleared using Next.js `NextResponse` helpers in API routes. --- ## 5. Auth Endpoints (Core) The core auth endpoints handle login and logout using the session cookie. ### 5.1 `POST /api/auth/login` **Purpose:** Authenticate a user using `username` and `password`, create a session, and set the session cookie. **Method & URL:** - `POST /api/auth/login` **Request Body (JSON):** ```json { "username": "example.user", "password": "plain-text-password" } ``` - `username` (string): Login name (case-insensitive). - `password` (string): Plaintext password entered by the user. **Behavior:** 1. Normalize `username` (trim + lowercase). 2. Look up the user in MongoDB by `username`. 3. If user not found → return `401` with `{ "error": "Invalid credentials" }`. 4. Verify the password using bcrypt (compare with `passwordHash`). 5. If password does not match → return `401` with `{ "error": "Invalid credentials" }`. 6. On success: - Create a session payload `{ userId, role, branchId }`. - Sign a JWT using `SESSION_SECRET`. - Set the JWT in the `auth_session` HTTP-only cookie. - Return `200` with `{ "ok": true }`. **Successful Response (200):** ```json { "ok": true } ``` (Session cookie is set in the response headers.) **Error Responses:** - `400 Bad Request`: - Missing `username` or `password`. - Invalid body format. - `401 Unauthorized`: - User not found. - Password does not match. - `500 Internal Server Error`: - Unexpected server-side error. ### 5.2 `GET /api/auth/logout` **Purpose:** Destroy the current session by clearing the session cookie. **Method & URL:** - `GET /api/auth/logout` **Request:** - No request body. - Uses the current session cookie (if present). **Behavior:** 1. Clear the `auth_session` cookie (e.g. by setting an expired cookie). 2. Return `200` with `{ "ok": true }`. Logout is **idempotent**: - If the cookie does not exist, the endpoint still returns `{ "ok": true }`. **Response (200):** ```json { "ok": true } ``` --- ## 6. Password Management & Recovery (Planned) This section describes the **planned** password management and recovery flows. The database model is already prepared for these scenarios, even if the endpoints are not yet implemented. ### 6.1 Change Password **Endpoint:** `POST /api/auth/change-password` **Status:** Planned. **Purpose:** Allow logged-in users to change their password by providing the current password and a new password. **Method & URL:** - `POST /api/auth/change-password` **Authentication:** - Requires a valid session (user must be logged in). **Request Body (JSON):** ```json { "currentPassword": "old-password", "newPassword": "new-password" } ``` **Behavior (planned):** 1. Extract `userId` from the current session. 2. Load user from MongoDB. 3. Verify `currentPassword` against `passwordHash` using bcrypt. 4. If verification fails → return `400` or `401` with a generic error (e.g. `{ "error": "Invalid password" }`). 5. Hash `newPassword` with bcrypt. 6. Update `passwordHash` in the database. 7. Optionally set `mustChangePassword = false`. 8. Optionally update a `passwordChangedAt` field if introduced later. 9. Return `{ "ok": true }`. **Response (200):** ```json { "ok": true } ``` ### 6.2 Request Password Reset **Endpoint:** `POST /api/auth/request-password-reset` **Status:** Planned. **Purpose:** Start the "forgot password" flow by sending a reset link to the user's email address. **Method & URL:** - `POST /api/auth/request-password-reset` **Request Body (JSON):** ```json { "usernameOrEmail": "nl01@company.com" } ``` - The frontend may allow either username or email. The backend resolves it accordingly. **Behavior (planned):** 1. Normalize the identifier (trim + lowercase). 2. Try to find a user by `email` (and optionally by `username` if needed). 3. If no user is found: - Do **not** reveal this to the caller. - Return a generic success response (e.g. `{ "ok": true }`) to avoid user enumeration. 4. If a user is found: - Generate a secure random token (or a signed token). - Store it in `passwordResetToken`. - Set `passwordResetExpiresAt` to a timestamp in the near future (e.g. now + 30 minutes). - Send an email to `user.email` containing a link like: ``` https:///reset-password?token= ``` - The email is sent using a mailer (e.g. `nodemailer`). 5. Always return `{ "ok": true }` to the client, regardless of whether a user was found. **Response (200):** ```json { "ok": true } ``` ### 6.3 Reset Password **Endpoint:** `POST /api/auth/reset-password` **Status:** Planned. **Purpose:** Complete the password reset process using a valid reset token. **Method & URL:** - `POST /api/auth/reset-password` **Request Body (JSON):** ```json { "token": "reset-token-from-email", "newPassword": "new-password" } ``` **Behavior (planned):** 1. Find user by `passwordResetToken`. 2. If no user is found → return a generic error (e.g. `{ "error": "Invalid or expired token" }`). 3. Check that `passwordResetExpiresAt` is in the future. 4. If the token has expired → return a generic error and clear token/expiry fields. 5. If the token is valid: - Hash `newPassword` with bcrypt. - Update `passwordHash` in the database. - Clear `passwordResetToken` and `passwordResetExpiresAt`. - Optionally set `mustChangePassword = false`. 6. Optionally invalidate other active sessions if a "global logout on password change" is implemented. 7. Return `{ "ok": true }`. **Response (200):** ```json { "ok": true } ``` ### 6.4 Email Sending Password reset emails will be sent using a mailer library (e.g. `nodemailer`), configured for the environment. Key points: - Emails are sent to `user.email`. - The content includes: - A short explanation of the password reset process. - A one-time link containing the `passwordResetToken`. - Information about the expiration time. - No confidential data (like passwords) is ever sent via email. --- ## 7. Security Considerations 1. **Never trust client-provided `branchId`.** - The effective `branchId` for authorization must always come from the **session payload** (derived from the user record), not from query parameters or request bodies. - Even if routes use `branch` parameters for URL structure, the backend must enforce access based on the `branchId` in the session. 2. **Password handling.** - Always hash passwords using a strong algorithm (e.g. bcrypt with a reasonable cost factor). - Never log plaintext passwords. - Never expose `passwordHash` or `passwordResetToken` in API responses. 3. **Session security.** - Use `httpOnly` cookies to protect the session token from JavaScript access. - Use `secure` cookies in production. - Use `sameSite: "lax"` or stricter unless cross-site needs are explicitly identified. - Use a strong `SESSION_SECRET`, rotated when necessary. 4. **Brute force and enumeration.** - Login and password reset endpoints should: - Respond with generic error messages (e.g. “Invalid credentials”). - Not leak information on whether a user exists. - Optionally implement rate limiting or throttling. 5. **Auditing and logging.** - Sensitive operations (login failures, password changes, password reset requests) should be logged with appropriate details, without exposing secrets. - Logs must not contain plaintext passwords or reset tokens. --- ## 8. Open Points & Future Work - Implement the session utility (`lib/auth/session.js`) with: - `createSession({ userId, role, branchId })` - `getSession()` - `destroySession()` - Implement the login and logout endpoints as described above. - Implement password management endpoints: - `POST /api/auth/change-password` - `POST /api/auth/request-password-reset` - `POST /api/auth/reset-password` - Implement email sending for password reset using `nodemailer` or similar. - Implement a UI for: - Login - Logout - Change password - “Forgot password” / reset password flows.