auth.md 11 KB

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.

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

Non-goals (for this document):

  • Email-based password recovery (documented as a follow-up ticket/phase).
  • User management endpoints (planned as RHL-012; this doc only defines the required permission semantics).

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:

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 (new capability, used by RHL-012)

    • Governs whether a user can create/update/manage 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:

{
	"error": {
		"message": "Human readable message",
		"code": "SOME_MACHINE_CODE",
		"details": {}
	}
}

Common branch RBAC responses:

  • 401 AUTH_UNAUTHENTICATED

    { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
    
  • 403 AUTH_FORBIDDEN_BRANCH

    { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } }
    

4.2 User management capability (RHL-012 prerequisite)

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

    {
    	"error": {
    		"message": "Forbidden",
    		"code": "AUTH_FORBIDDEN_USER_MANAGEMENT"
    	}
    }
    

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

{
	"userId": "<MongoDB ObjectId as string>",
	"role": "branch | admin | superadmin | dev",
	"branchId": "NL01 | null",
	"email": "name@company.tld | null",
	"iat": 1700000000,
	"exp": 1700003600
}

Notes:

  • email is optional and may be null.
  • The session email is used for displaying read-only account information in the UI (Profile).

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 })
  • 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)

    {
    	"error": {
    		"message": "Invalid request body",
    		"code": "VALIDATION_INVALID_JSON"
    	}
    }
    
  • 400 (missing username/password)

    {
    	"error": {
    		"message": "Missing username or password",
    		"code": "VALIDATION_MISSING_FIELD",
    		"details": { "fields": ["username", "password"] }
    	}
    }
    
  • 401 (invalid credentials)

    {
    	"error": {
    		"message": "Invalid credentials",
    		"code": "AUTH_INVALID_CREDENTIALS"
    	}
    }
    
  • 500

    {
    	"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):

{ "user": null }

Response (authenticated):

{
	"user": {
		"userId": "...",
		"role": "branch|admin|superadmin|dev",
		"branchId": "NL01",
		"email": "nl01@example.com"
	}
}

Notes:

  • email is optional and may be null.
  • 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):

{
	"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.4 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).