auth.md 15 KB

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:

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

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

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

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

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

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

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

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

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

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