Bläddra i källkod

RHL-003-doc(auth): add comprehensive authentication and authorization documentation

Code_Uwe 1 vecka sedan
förälder
incheckning
f9dbe68bf5
1 ändrade filer med 551 tillägg och 0 borttagningar
  1. 551 0
      Docs/auth.md

+ 551 - 0
Docs/auth.md

@@ -0,0 +1,551 @@
+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": "<MongoDB ObjectId as string>",
+	"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://<app-domain>/reset-password?token=<passwordResetToken>
+     ```
+
+   - 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.