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.
This document describes the authentication and authorization model for the internal delivery note browser.
The system uses:
branch, admin, dev).NOTE: This document is a living document. As we extend the auth system (sessions, routes, policies, password flows), we will update this file.
The main goals of the authentication system are:
This document covers:
Users are stored in MongoDB using the User collection.
username (String, required, unique, lowercased)
email (String, required, unique, lowercased)
passwordHash (String, required)
role (String, required, enum: "branch" | "admin" | "dev")
branchId (String | null)
"NL01") that the user belongs to.role = "branch".null or unused for non-branch users (admin, dev).mustChangePassword (Boolean, default: false)
true, the user should be forced to set a new password on the next login.passwordResetToken (String | null)
null if there is no active reset request.passwordResetExpiresAt (Date | null)
passwordResetToken.null if there is no active reset request.createdAt (Date, auto-generated)
updatedAt (Date, auto-generated)
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.role = "branch", branchId must be a non-empty string.role = "admin" and role = "dev", branchId is optional and usually null.passwordResetToken and passwordResetExpiresAt must be consistent:
When converting User documents to JSON or plain objects (e.g. in API responses), the following fields must be hidden:
passwordHashpasswordResetTokenThis ensures that sensitive information is not exposed via API responses or logs.
branchbranchId (e.g. "NL01").adminbranchId = null).devbranchId = null).role is set by the admin.branchId is set by the admin and cannot be chosen by the user.role = "branch"branchId set to the respective branch identifier.Sessions are implemented as signed JWTs stored in HTTP-only cookies.
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.
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):
The session token is stored in an HTTP-only cookie, for example:
auth_session (TBD, but must be consistent across backend/frontend)Attributes:
httpOnly: truesecure: 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.
The core auth endpoints handle login and logout using the session cookie.
POST /api/auth/loginPurpose:
Authenticate a user using username and password, create a session, and set the session cookie.
Method & URL:
POST /api/auth/loginRequest Body (JSON):
{
"username": "example.user",
"password": "plain-text-password"
}
username (string): Login name (case-insensitive).password (string): Plaintext password entered by the user.Behavior:
username (trim + lowercase).username.401 with { "error": "Invalid credentials" }.passwordHash).401 with { "error": "Invalid credentials" }.On success:
{ userId, role, branchId }.SESSION_SECRET.auth_session HTTP-only cookie.200 with { "ok": true }.Successful Response (200):
{
"ok": true
}
(Session cookie is set in the response headers.)
Error Responses:
400 Bad Request:
username or password.401 Unauthorized:
500 Internal Server Error:
GET /api/auth/logoutPurpose: Destroy the current session by clearing the session cookie.
Method & URL:
GET /api/auth/logoutRequest:
Behavior:
auth_session cookie (e.g. by setting an expired cookie).200 with { "ok": true }.Logout is idempotent:
{ "ok": true }.Response (200):
{
"ok": true
}
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.
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-passwordAuthentication:
Request Body (JSON):
{
"currentPassword": "old-password",
"newPassword": "new-password"
}
Behavior (planned):
userId from the current session.currentPassword against passwordHash using bcrypt.400 or 401 with a generic error (e.g. { "error": "Invalid password" }).newPassword with bcrypt.passwordHash in the database.mustChangePassword = false.passwordChangedAt field if introduced later.{ "ok": true }.Response (200):
{
"ok": true
}
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-resetRequest Body (JSON):
{
"usernameOrEmail": "nl01@company.com"
}
Behavior (planned):
Normalize the identifier (trim + lowercase).
Try to find a user by email (and optionally by username if needed).
If no user is found:
{ "ok": true }) to avoid user enumeration.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).
Always return { "ok": true } to the client, regardless of whether a user was found.
Response (200):
{
"ok": true
}
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-passwordRequest Body (JSON):
{
"token": "reset-token-from-email",
"newPassword": "new-password"
}
Behavior (planned):
passwordResetToken.{ "error": "Invalid or expired token" }).passwordResetExpiresAt is in the future.If the token is valid:
newPassword with bcrypt.passwordHash in the database.passwordResetToken and passwordResetExpiresAt.mustChangePassword = false.Optionally invalidate other active sessions if a "global logout on password change" is implemented.
Return { "ok": true }.
Response (200):
{
"ok": true
}
Password reset emails will be sent using a mailer library (e.g. nodemailer), configured for the environment.
Key points:
user.email.The content includes:
passwordResetToken.No confidential data (like passwords) is ever sent via email.
Never trust client-provided branchId.
branchId for authorization must always come from the session payload (derived from the user record), not from query parameters or request bodies.branch parameters for URL structure, the backend must enforce access based on the branchId in the session.Password handling.
passwordHash or passwordResetToken in API responses.Session security.
httpOnly cookies to protect the session token from JavaScript access.secure cookies in production.sameSite: "lax" or stricter unless cross-site needs are explicitly identified.SESSION_SECRET, rotated when necessary.Brute force and enumeration.
Login and password reset endpoints should:
Auditing and logging.
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-passwordPOST /api/auth/request-password-resetPOST /api/auth/reset-passwordImplement email sending for password reset using nodemailer or similar.
Implement a UI for: