# 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.
- A separate **user management capability** for admin APIs (RHL-041 + RHL-012).

---

## 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).
- Must-change-password enforcement semantics (`mustChangePassword`).

Non-goals (for this document):

- Email-based password recovery (documented as a follow-up ticket/phase).
- Full user management API contract details (documented in `Docs/api.md` and `Docs/frontend-api-usage.md`).

---

## 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** (RHL-012)
   - Governs whether a user can create/update/delete 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)

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"
  	}
  }
  ```

Where it is used:

- User management endpoints under `/api/admin/users` (see `Docs/api.md`).

### 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": "<MongoDB ObjectId as string>",
	"role": "branch | admin | superadmin | dev",
	"branchId": "NL01 | null",
	"email": "name@company.tld | null",
	"mustChangePassword": "boolean",
	"iat": 1700000000,
	"exp": 1700003600
}
```

Notes:

- `email` is optional and may be `null`.
- `mustChangePassword` is always normalized to a boolean (`true` only when explicitly set to `true`; otherwise `false`).
- The session email is used for displaying read-only account information in the UI (Profile).
- `mustChangePassword` is consumed by frontend auth gating (RHL-044).

### 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, mustChangePassword })`
- `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",
		"mustChangePassword": true
	}
}
```

Notes:

- `email` is optional and may be `null`.
- `mustChangePassword` is always returned as a boolean.
- 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": "<string>",
	"newPassword": "<string>"
}
```

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. Refresh the session cookie with `mustChangePassword = false` so frontend revalidation immediately sees the cleared gate state.

### 7.4 Must-change-password enforcement (RHL-044)

Frontend behavior for authenticated users with `mustChangePassword = true`:

- The protected UI is gated centrally via `AuthGate`.
- Users are redirected to `/profile` until the password is changed.
- `/profile` displays a clear required-action alert in German.
- After a successful password change:
  - backend clears `mustChangePassword`
  - backend refreshes the session cookie
  - frontend revalidates auth state and automatically lifts the gate

### 7.5 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)`.
