|
@@ -1,10 +1,10 @@
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
|
|
-<!-- Folder: Docs -->
|
|
|
|
|
|
|
+<!-- Ordner: Docs -->
|
|
|
|
|
|
|
|
-<!-- File: auth.md -->
|
|
|
|
|
|
|
+<!-- Datei: auth.md -->
|
|
|
|
|
|
|
|
-<!-- Relative path: Docs/auth.md -->
|
|
|
|
|
|
|
+<!-- Relativer Pfad: Docs/auth.md -->
|
|
|
|
|
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
|
@@ -14,11 +14,10 @@ This document describes the authentication and authorization model for the inter
|
|
|
|
|
|
|
|
The system uses:
|
|
The system uses:
|
|
|
|
|
|
|
|
-- MongoDB to store users.
|
|
|
|
|
|
|
+- MongoDB to store users (via Mongoose models).
|
|
|
- Cookie-based sessions with a signed JWT payload.
|
|
- Cookie-based sessions with a signed JWT payload.
|
|
|
- Role-aware access control (`branch`, `admin`, `dev`).
|
|
- Role-aware access control (`branch`, `admin`, `dev`).
|
|
|
-- Branch-level RBAC enforcement for filesystem APIs.
|
|
|
|
|
-- Extensible password management and recovery flows.
|
|
|
|
|
|
|
+- Branch-level RBAC enforcement for filesystem-related APIs.
|
|
|
|
|
|
|
|
> NOTE: This document is a living document. As we extend the auth system (sessions, routes, policies, password flows), we will update this file.
|
|
> NOTE: This document is a living document. As we extend the auth system (sessions, routes, policies, password flows), we will update this file.
|
|
|
|
|
|
|
@@ -33,183 +32,101 @@ The main goals of the authentication and authorization system are:
|
|
|
- Admin and dev users can access data across branches.
|
|
- Admin and dev users can access data across branches.
|
|
|
- Passwords are never stored in plaintext.
|
|
- Passwords are never stored in plaintext.
|
|
|
- Sessions are stored as signed JWTs in HTTP-only cookies.
|
|
- Sessions are stored as signed JWTs in HTTP-only cookies.
|
|
|
-- The system is ready for password change and password recovery functionality.
|
|
|
|
|
|
|
|
|
|
This document covers:
|
|
This document covers:
|
|
|
|
|
|
|
|
-- User model and roles.
|
|
|
|
|
- Environment variables related to auth.
|
|
- Environment variables related to auth.
|
|
|
-- RBAC rules and protected filesystem endpoints.
|
|
|
|
|
|
|
+- Roles and RBAC rules.
|
|
|
- Session payload and cookie configuration.
|
|
- Session payload and cookie configuration.
|
|
|
- Login and logout endpoints.
|
|
- Login and logout endpoints.
|
|
|
-- Planned endpoints for password management and recovery.
|
|
|
|
|
-- Security considerations and implementation guidelines.
|
|
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 2. Environment Variables
|
|
|
|
|
|
|
+## 2. Environment & Configuration
|
|
|
|
|
|
|
|
-The authentication system depends on the following environment variables:
|
|
|
|
|
|
|
+### 2.1 Required variables
|
|
|
|
|
+
|
|
|
|
|
+Auth depends on the following environment variables:
|
|
|
|
|
|
|
|
- `SESSION_SECRET` (required)
|
|
- `SESSION_SECRET` (required)
|
|
|
|
|
|
|
|
- Strong, random string used to sign and verify JWT session tokens.
|
|
- Strong, random string used to sign and verify JWT session tokens.
|
|
|
- - Must be kept secret and should differ between environments (dev, staging, prod).
|
|
|
|
|
-
|
|
|
|
|
-Example for `.env.local.example`:
|
|
|
|
|
-
|
|
|
|
|
-```env
|
|
|
|
|
-# Session / JWT
|
|
|
|
|
-SESSION_SECRET=change-me-to-a-long-random-string
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-If `SESSION_SECRET` is not set, session utilities will throw an error.
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 3. User Model
|
|
|
|
|
-
|
|
|
|
|
-Users are stored in MongoDB using the `User` collection.
|
|
|
|
|
-
|
|
|
|
|
-### 3.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.
|
|
|
|
|
- - For branch accounts, this is typically the branch email address.
|
|
|
|
|
- - For individual accounts, this can be the personal work email.
|
|
|
|
|
- - 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`)
|
|
|
|
|
|
|
+ - Minimum length: **32 characters**.
|
|
|
|
|
+ - Must be kept secret.
|
|
|
|
|
+ - Should differ between environments (dev/staging/prod).
|
|
|
|
|
|
|
|
- - 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.
|
|
|
|
|
|
|
+Auth endpoints also require DB connectivity:
|
|
|
|
|
|
|
|
-- **createdAt** (`Date`, auto-generated)
|
|
|
|
|
|
|
+- `MONGODB_URI` (required)
|
|
|
|
|
|
|
|
- - Timestamp when the user record was created.
|
|
|
|
|
|
|
+### 2.2 Optional variables
|
|
|
|
|
|
|
|
-- **updatedAt** (`Date`, auto-generated)
|
|
|
|
|
|
|
+- `SESSION_COOKIE_SECURE` (optional)
|
|
|
|
|
|
|
|
- - Timestamp when the user record was last updated.
|
|
|
|
|
|
|
+ - Overrides the `Secure` cookie flag.
|
|
|
|
|
+ - Allowed values: `true` or `false`.
|
|
|
|
|
|
|
|
-### 3.2 Validation Rules & Invariants
|
|
|
|
|
|
|
+Default behavior:
|
|
|
|
|
|
|
|
-- `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` should be consistent:
|
|
|
|
|
|
|
+- `Secure` cookie is enabled when `NODE_ENV === "production"`.
|
|
|
|
|
|
|
|
- - If one is set, the other should also be set.
|
|
|
|
|
- - Once a reset is completed or expired, both should be cleared.
|
|
|
|
|
|
|
+Local HTTP testing (e.g. `http://localhost:3000` with Docker + `next start`):
|
|
|
|
|
|
|
|
-### 3.3 Serialization Rules
|
|
|
|
|
|
|
+- Set `SESSION_COOKIE_SECURE=false` in your local `.env.docker`.
|
|
|
|
|
|
|
|
-When converting `User` documents to JSON or plain objects (e.g. in API responses), the following fields must be hidden:
|
|
|
|
|
|
|
+Staging/Production:
|
|
|
|
|
|
|
|
-- `passwordHash`
|
|
|
|
|
-- `passwordResetToken`
|
|
|
|
|
|
|
+- Keep `SESSION_COOKIE_SECURE` unset (or `true`) and run the app behind HTTPS.
|
|
|
|
|
|
|
|
-This ensures that sensitive information is not exposed via API responses or logs.
|
|
|
|
|
|
|
+### 2.3 Fail-fast environment validation
|
|
|
|
|
|
|
|
-### 3.4 Role Assignment & User Provisioning
|
|
|
|
|
|
|
+The repo provides centralized env validation:
|
|
|
|
|
|
|
|
-- Users are **created by an admin** (no public self-registration).
|
|
|
|
|
|
|
+- `lib/config/validateEnv.js` validates required env vars and basic sanity checks.
|
|
|
|
|
+- `scripts/validate-env.mjs` runs validation against `process.env`.
|
|
|
|
|
|
|
|
-- When a user is created:
|
|
|
|
|
|
|
+In Docker, run validation before starting the server:
|
|
|
|
|
|
|
|
- - `role` is set by the admin.
|
|
|
|
|
- - `branchId` is set by the admin and cannot be chosen or changed 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 (e.g. `"NL01"`).
|
|
|
|
|
-
|
|
|
|
|
-- The user is provided with an initial password and is encouraged (or forced via `mustChangePassword`) to change it after the first login.
|
|
|
|
|
|
|
+```sh
|
|
|
|
|
+node scripts/validate-env.mjs && npm run start
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 4. Roles
|
|
|
|
|
|
|
+## 3. Roles
|
|
|
|
|
|
|
|
-### 4.1 `branch`
|
|
|
|
|
|
|
+### 3.1 `branch`
|
|
|
|
|
|
|
|
- Represents a user who belongs to a specific branch/location.
|
|
- Represents a user who belongs to a specific branch/location.
|
|
|
- Must have a valid `branchId` (e.g. `"NL01"`).
|
|
- Must have a valid `branchId` (e.g. `"NL01"`).
|
|
|
-- Intended access pattern (high-level):
|
|
|
|
|
|
|
+- Intended access pattern:
|
|
|
|
|
|
|
|
- Can only access delivery notes for their own branch.
|
|
- Can only access delivery notes for their own branch.
|
|
|
- Cannot access other branches.
|
|
- Cannot access other branches.
|
|
|
- - No global configuration or system-wide administration.
|
|
|
|
|
|
|
|
|
|
-### 4.2 `admin`
|
|
|
|
|
|
|
+### 3.2 `admin`
|
|
|
|
|
|
|
|
-- System administrator.
|
|
|
|
|
|
|
+- Administrator account.
|
|
|
- Typically not bound to any single branch (`branchId = null`).
|
|
- Typically not bound to any single branch (`branchId = null`).
|
|
|
-- Intended access pattern (high-level):
|
|
|
|
|
|
|
+- Intended access pattern:
|
|
|
|
|
|
|
|
- Can access delivery notes across all branches.
|
|
- Can access delivery notes across all branches.
|
|
|
- - Can perform user administration (create/update users).
|
|
|
|
|
- - Can perform configuration-level changes.
|
|
|
|
|
|
|
|
|
|
-### 4.3 `dev`
|
|
|
|
|
|
|
+### 3.3 `dev`
|
|
|
|
|
|
|
|
- Development/engineering account.
|
|
- Development/engineering account.
|
|
|
-- Used for debugging, maintenance, and operational tooling.
|
|
|
|
|
- Typically not bound to any single branch (`branchId = null`).
|
|
- Typically not bound to any single branch (`branchId = null`).
|
|
|
-- Intended access pattern (high-level):
|
|
|
|
|
|
|
+- Intended access pattern:
|
|
|
|
|
|
|
|
- - Full or near-full access to the system.
|
|
|
|
|
- - Can be used in development/staging environments.
|
|
|
|
|
- - Production use should be limited and auditable.
|
|
|
|
|
|
|
+ - Full or near-full access.
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 5. Authorization: Branch-Level RBAC
|
|
|
|
|
|
|
+## 4. Authorization: Branch-Level RBAC
|
|
|
|
|
|
|
|
-The backend enforces **Role-Based Access Control (RBAC)** on branch-related filesystem APIs.
|
|
|
|
|
|
|
+RBAC is enforced on branch-related filesystem APIs.
|
|
|
|
|
|
|
|
-### 5.1 Response Semantics
|
|
|
|
|
|
|
+### 4.1 Response semantics
|
|
|
|
|
|
|
|
- **401 Unauthorized**: no valid session (`getSession()` returns `null`).
|
|
- **401 Unauthorized**: no valid session (`getSession()` returns `null`).
|
|
|
|
|
|
|
@@ -223,64 +140,30 @@ The backend enforces **Role-Based Access Control (RBAC)** on branch-related file
|
|
|
{ "error": "Forbidden" }
|
|
{ "error": "Forbidden" }
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-> Note: Some legacy `400`/`500` messages are still returned in German (e.g. missing params, filesystem errors). We may normalize these later.
|
|
|
|
|
|
|
+### 4.2 Permission helpers
|
|
|
|
|
|
|
|
-### 5.2 Permission Helpers
|
|
|
|
|
-
|
|
|
|
|
-RBAC rules are implemented in `lib/auth/permissions.js`:
|
|
|
|
|
|
|
+RBAC rules live in `lib/auth/permissions.js`:
|
|
|
|
|
|
|
|
- `canAccessBranch(session, branchId)`
|
|
- `canAccessBranch(session, branchId)`
|
|
|
-
|
|
|
|
|
- - No session → `false`
|
|
|
|
|
- - `role = "branch"` → `true` only if `session.branchId === branchId`
|
|
|
|
|
- - `role = "admin" | "dev"` → `true` for any branch
|
|
|
|
|
-
|
|
|
|
|
- `filterBranchesForSession(session, branchIds)`
|
|
- `filterBranchesForSession(session, branchIds)`
|
|
|
|
|
|
|
|
- - `role = "branch"` → returns only the user’s own branch (if present)
|
|
|
|
|
- - `role = "admin" | "dev"` → returns all
|
|
|
|
|
|
|
+### 4.3 Protected endpoints
|
|
|
|
|
|
|
|
-### 5.3 Protected Filesystem APIs
|
|
|
|
|
-
|
|
|
|
|
-The following endpoints are protected and must be called only with a valid session:
|
|
|
|
|
|
|
+These endpoints require a valid session:
|
|
|
|
|
|
|
|
- `GET /api/branches`
|
|
- `GET /api/branches`
|
|
|
-
|
|
|
|
|
- - Requires session (401 otherwise)
|
|
|
|
|
- - `branch` role: returns only `[session.branchId]`
|
|
|
|
|
- - `admin`/`dev`: returns all branches
|
|
|
|
|
-
|
|
|
|
|
- `GET /api/branches/[branch]/years`
|
|
- `GET /api/branches/[branch]/years`
|
|
|
-
|
|
|
|
|
- `GET /api/branches/[branch]/[year]/months`
|
|
- `GET /api/branches/[branch]/[year]/months`
|
|
|
-
|
|
|
|
|
- `GET /api/branches/[branch]/[year]/[month]/days`
|
|
- `GET /api/branches/[branch]/[year]/[month]/days`
|
|
|
-
|
|
|
|
|
- - Requires session (401 otherwise)
|
|
|
|
|
- - Requires branch access (403 if not allowed)
|
|
|
|
|
-
|
|
|
|
|
- `GET /api/files?branch=&year=&month=&day=`
|
|
- `GET /api/files?branch=&year=&month=&day=`
|
|
|
|
|
|
|
|
- - Requires session (401 otherwise)
|
|
|
|
|
- - Requires branch access (403 if not allowed)
|
|
|
|
|
-
|
|
|
|
|
-Implementation pattern (high-level):
|
|
|
|
|
-
|
|
|
|
|
-1. `const session = await getSession()`
|
|
|
|
|
-2. If `!session` → return 401
|
|
|
|
|
-3. Extract requested branch (`params.branch` or `query.branch`)
|
|
|
|
|
-4. If `!canAccessBranch(session, requestedBranch)` → return 403
|
|
|
|
|
-5. Proceed with storage access and return data
|
|
|
|
|
-
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 6. Sessions & Cookies
|
|
|
|
|
|
|
+## 5. Sessions & Cookies
|
|
|
|
|
|
|
|
Sessions are implemented as signed JWTs stored in HTTP-only cookies.
|
|
Sessions are implemented as signed JWTs stored in HTTP-only cookies.
|
|
|
|
|
|
|
|
-### 6.1 Session Payload Format
|
|
|
|
|
-
|
|
|
|
|
-A session payload has the following structure:
|
|
|
|
|
|
|
+### 5.1 Session payload
|
|
|
|
|
|
|
|
```json
|
|
```json
|
|
|
{
|
|
{
|
|
@@ -292,403 +175,57 @@ A session payload has the following structure:
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-- `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.
|
|
|
|
|
-
|
|
|
|
|
-### 6.2 JWT Signing
|
|
|
|
|
-
|
|
|
|
|
-- JWTs are signed using a symmetric secret (`SESSION_SECRET`).
|
|
|
|
|
-- Algorithm: `HS256` (HMAC using SHA-256).
|
|
|
|
|
-- Secret is defined via environment variable `SESSION_SECRET`.
|
|
|
|
|
-- Token lifetime:
|
|
|
|
|
-
|
|
|
|
|
- - `SESSION_MAX_AGE_SECONDS = 60 * 60 * 8` (8 hours).
|
|
|
|
|
- - Configured in `lib/auth/session.js`.
|
|
|
|
|
-
|
|
|
|
|
-### 6.3 Cookie Settings
|
|
|
|
|
-
|
|
|
|
|
-The session token is stored in an HTTP-only cookie with the following properties:
|
|
|
|
|
-
|
|
|
|
|
-- **Cookie name**: `auth_session`
|
|
|
|
|
-- **Attributes**:
|
|
|
|
|
-
|
|
|
|
|
- - `httpOnly: true`
|
|
|
|
|
- - `secure: process.env.NODE_ENV === "production"`
|
|
|
|
|
- - `sameSite: "lax"`
|
|
|
|
|
- - `path: "/"` (cookie is sent for all paths)
|
|
|
|
|
- - `maxAge: 8 hours` (matching `SESSION_MAX_AGE_SECONDS`)
|
|
|
|
|
-
|
|
|
|
|
-Cookies are written and cleared using Next.js `cookies()` from `next/headers` inside `lib/auth/session.js`:
|
|
|
|
|
-
|
|
|
|
|
-- `createSession({ userId, role, branchId })`:
|
|
|
|
|
-
|
|
|
|
|
- - Creates and signs a JWT.
|
|
|
|
|
- - Sets the `auth_session` cookie.
|
|
|
|
|
-
|
|
|
|
|
-- `getSession()`:
|
|
|
|
|
-
|
|
|
|
|
- - Reads the `auth_session` cookie.
|
|
|
|
|
- - Verifies the JWT and returns `{ userId, role, branchId }` or `null`.
|
|
|
|
|
- - If the token is invalid or expired, clears the cookie and returns `null`.
|
|
|
|
|
-
|
|
|
|
|
-- `destroySession()`:
|
|
|
|
|
-
|
|
|
|
|
- - Clears the `auth_session` cookie by setting an empty value with `maxAge: 0`.
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 7. Core Auth Endpoints
|
|
|
|
|
-
|
|
|
|
|
-### 7.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 whitespace and convert to lowercase.
|
|
|
|
|
-
|
|
|
|
|
-2. Parse and validate request body:
|
|
|
|
|
-
|
|
|
|
|
- - If body is missing or invalid JSON → `400 { "error": "Invalid request body" }`.
|
|
|
|
|
- - If `username` or `password` is missing or empty → `400 { "error": "Missing username or password" }`.
|
|
|
|
|
-
|
|
|
|
|
-3. Connect to MongoDB.
|
|
|
|
|
-
|
|
|
|
|
-4. Look up the user in MongoDB by normalized `username`.
|
|
|
|
|
-
|
|
|
|
|
- - If no user is found → `401 { "error": "Invalid credentials" }`.
|
|
|
|
|
-
|
|
|
|
|
-5. Verify the password using bcrypt:
|
|
|
|
|
-
|
|
|
|
|
- - Compare provided `password` with `user.passwordHash`.
|
|
|
|
|
- - If password does not match → `401 { "error": "Invalid credentials" }`.
|
|
|
|
|
-
|
|
|
|
|
-6. On success:
|
|
|
|
|
-
|
|
|
|
|
- - Create a session payload `{ userId, role, branchId }`.
|
|
|
|
|
-
|
|
|
|
|
- - Call `createSession({ userId, role, branchId })`:
|
|
|
|
|
-
|
|
|
|
|
- - Signs a JWT with the session payload.
|
|
|
|
|
- - Sets the `auth_session` HTTP-only cookie.
|
|
|
|
|
-
|
|
|
|
|
- - Return `200 { "ok": true }`.
|
|
|
|
|
-
|
|
|
|
|
-**Possible Responses**
|
|
|
|
|
-
|
|
|
|
|
-- `200 OK`:
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "ok": true
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
-- `400 Bad Request`:
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "error": "Invalid request body"
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
- or
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "error": "Missing username or password"
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
-- `401 Unauthorized`:
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "error": "Invalid credentials"
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
-- `500 Internal Server Error`:
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "error": "Internal server error"
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
-### 7.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. Call `destroySession()`:
|
|
|
|
|
-
|
|
|
|
|
- - Clears the `auth_session` cookie by setting an empty value with `maxAge: 0`.
|
|
|
|
|
-
|
|
|
|
|
-2. Return `200 { "ok": true }`.
|
|
|
|
|
-
|
|
|
|
|
-Logout is **idempotent**:
|
|
|
|
|
-
|
|
|
|
|
-- If the cookie does not exist, the endpoint still returns `{ "ok": true }`.
|
|
|
|
|
-
|
|
|
|
|
-**Responses**
|
|
|
|
|
-
|
|
|
|
|
-- `200 OK`:
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "ok": true
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
-- `500 Internal Server Error` (if `destroySession` throws):
|
|
|
|
|
-
|
|
|
|
|
- ```json
|
|
|
|
|
- {
|
|
|
|
|
- "error": "Internal server error"
|
|
|
|
|
- }
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 8. Password Management & Recovery (Planned)
|
|
|
|
|
-
|
|
|
|
|
-The database model is already prepared for password management and password recovery flows, but the respective endpoints may be implemented in a separate epic.
|
|
|
|
|
-
|
|
|
|
|
-### 8.1 Change Password
|
|
|
|
|
-
|
|
|
|
|
-**Endpoint**
|
|
|
|
|
-`POST /api/auth/change-password` (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"
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Planned Behavior**
|
|
|
|
|
-
|
|
|
|
|
-1. Extract `userId` from the current session (`getSession()`).
|
|
|
|
|
-2. Load user from MongoDB.
|
|
|
|
|
-3. Verify `currentPassword` against `passwordHash` using bcrypt.
|
|
|
|
|
-4. If verification fails → return a generic error (e.g. `400` or `401` with `{ "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 }`.
|
|
|
|
|
-
|
|
|
|
|
-### 8.2 Request Password Reset
|
|
|
|
|
-
|
|
|
|
|
-**Endpoint**
|
|
|
|
|
-`POST /api/auth/request-password-reset` (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"
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Planned Behavior**
|
|
|
|
|
-
|
|
|
|
|
-1. Normalize the identifier (trim + lowercase).
|
|
|
|
|
-
|
|
|
|
|
-2. Try to find a user by `email` (and optionally by `username`).
|
|
|
|
|
-
|
|
|
|
|
-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>
|
|
|
|
|
- ```
|
|
|
|
|
-
|
|
|
|
|
-5. Always return `{ "ok": true }`.
|
|
|
|
|
-
|
|
|
|
|
-### 8.3 Reset Password
|
|
|
|
|
-
|
|
|
|
|
-**Endpoint**
|
|
|
|
|
-`POST /api/auth/reset-password` (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"
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Planned Behavior**
|
|
|
|
|
-
|
|
|
|
|
-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.
|
|
|
|
|
- - Clear `passwordResetToken` and `passwordResetExpiresAt`.
|
|
|
|
|
-
|
|
|
|
|
-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.
|
|
|
|
|
|
|
+### 5.2 JWT signing
|
|
|
|
|
|
|
|
-7. Return `{ "ok": true }`.
|
|
|
|
|
|
|
+- Algorithm: `HS256`.
|
|
|
|
|
+- Secret: `SESSION_SECRET`.
|
|
|
|
|
+- Token lifetime: `SESSION_MAX_AGE_SECONDS = 8 hours`.
|
|
|
|
|
|
|
|
-### 8.4 Email Sending
|
|
|
|
|
|
|
+### 5.3 Cookie settings
|
|
|
|
|
|
|
|
-Password reset emails will be sent using a mailer library (e.g. `nodemailer`), configured for the environment.
|
|
|
|
|
|
|
+Cookie name: `auth_session`
|
|
|
|
|
|
|
|
-Key points:
|
|
|
|
|
|
|
+Attributes:
|
|
|
|
|
|
|
|
-- Emails are sent to `user.email`.
|
|
|
|
|
|
|
+- `httpOnly: true`
|
|
|
|
|
+- `secure: resolved via NODE_ENV + optional SESSION_COOKIE_SECURE override`
|
|
|
|
|
+- `sameSite: "lax"`
|
|
|
|
|
+- `path: "/"`
|
|
|
|
|
+- `maxAge: 8 hours`
|
|
|
|
|
|
|
|
-- The content includes:
|
|
|
|
|
|
|
+Implementation lives in `lib/auth/session.js`:
|
|
|
|
|
|
|
|
- - 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.
|
|
|
|
|
|
|
+- `createSession({ userId, role, branchId })`
|
|
|
|
|
+- `getSession()`
|
|
|
|
|
+- `destroySession()`
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 9. Security Considerations
|
|
|
|
|
-
|
|
|
|
|
-1. **Never trust client-provided branch information.**
|
|
|
|
|
-
|
|
|
|
|
- - The effective branch authorization is enforced using the **session payload** (`session.branchId`) and RBAC rules.
|
|
|
|
|
- - Even if routes use `branch` parameters for URL structure, the backend enforces branch access based on the session.
|
|
|
|
|
|
|
+## 6. Core Auth Endpoints
|
|
|
|
|
|
|
|
-2. **Password handling.**
|
|
|
|
|
|
|
+### 6.1 `POST /api/auth/login`
|
|
|
|
|
|
|
|
- - 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.
|
|
|
|
|
|
|
+Authenticate a user and set the session cookie.
|
|
|
|
|
|
|
|
-3. **Session security.**
|
|
|
|
|
|
|
+Responses:
|
|
|
|
|
|
|
|
- - 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.
|
|
|
|
|
|
|
+- `200 { "ok": true }`
|
|
|
|
|
+- `400 { "error": "Invalid request body" }`
|
|
|
|
|
+- `400 { "error": "Missing username or password" }`
|
|
|
|
|
+- `401 { "error": "Invalid credentials" }`
|
|
|
|
|
+- `500 { "error": "Internal server error" }`
|
|
|
|
|
|
|
|
-4. **Brute force and enumeration.**
|
|
|
|
|
|
|
+### 6.2 `GET /api/auth/logout`
|
|
|
|
|
|
|
|
- - Login and password reset endpoints should:
|
|
|
|
|
|
|
+Clears the session cookie.
|
|
|
|
|
|
|
|
- - 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.
|
|
|
|
|
|
|
+- Returns `200 { "ok": true }` on success.
|
|
|
|
|
+- Logout is idempotent.
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 10. Future Work & Integration
|
|
|
|
|
-
|
|
|
|
|
-- **(Optional)** Add a `middleware.js` for frontend route protection (redirect unauthenticated users to login for certain pages).
|
|
|
|
|
-
|
|
|
|
|
-- Implement password management endpoints:
|
|
|
|
|
-
|
|
|
|
|
- - `POST /api/auth/change-password`
|
|
|
|
|
- - `POST /api/auth/request-password-reset`
|
|
|
|
|
- - `POST /api/auth/reset-password`
|
|
|
|
|
-
|
|
|
|
|
-- Integrate an email provider using `nodemailer` or similar for password reset.
|
|
|
|
|
-
|
|
|
|
|
-- Build frontend UI for:
|
|
|
|
|
-
|
|
|
|
|
- - Login
|
|
|
|
|
- - Logout
|
|
|
|
|
- - Change password
|
|
|
|
|
- - “Forgot password” / reset password flows.
|
|
|
|
|
-
|
|
|
|
|
-- Optional improvements:
|
|
|
|
|
|
|
+## 7. Security Notes
|
|
|
|
|
|
|
|
- - Normalize API error messages (language and structure) across all endpoints.
|
|
|
|
|
- - Add auditing for admin actions and branch access.
|
|
|
|
|
|
|
+- 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`.
|