|
|
@@ -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.
|