docs: keep demo identity replaceable

This commit is contained in:
2026-08-20 17:14:40 +03:30
parent 406cd9c0cf
commit 384ea54282
2 changed files with 25 additions and 9 deletions

View File

@@ -7,7 +7,7 @@
## Objective ## Objective
Allow hackathon judges and merchants to authenticate as distinct merchant users, while enforcing strict server-side tenant isolation. Seed one demo user per merchant through a secure one-time administrative workflow so different merchant dashboards can be demonstrated without adding self-service account management to the MVP. Allow hackathon judges and merchants to authenticate as distinct merchant users, while enforcing strict server-side tenant isolation. Seed one demo user per merchant through a secure one-time administrative workflow so different merchant dashboards can be demonstrated. This username/password capability is a deliberately small demo adapter: if the project is accepted, ZarinPal SSO replaces authentication while the reusable tenant-authorization boundary remains.
## Actors ## Actors
@@ -39,11 +39,13 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- User-created roles, permissions, or additional merchant users - User-created roles, permissions, or additional merchant users
- Cross-merchant operator access in the browser - Cross-merchant operator access in the browser
- Storing credentials, session tokens, or generated credential exports in Git - Storing credentials, session tokens, or generated credential exports in Git
- Implementing or simulating ZarinPal SSO before the project is accepted
- Production-grade identity lifecycle features that ZarinPal SSO will own
## Domain and data contract ## Domain and data contract
- A `Merchant User` belongs to exactly one merchant in the MVP. - A `Merchant User` belongs to exactly one merchant in the MVP.
- Usernames are unique after the approved normalization rule; the exact normalization remains open. - Demo usernames use stable lowercase ASCII identifiers such as `merchant-0001`. Comparison is case-insensitive, and rerunning the seed preserves an existing merchant's username.
- Passwords are stored only as Argon2id encoded hashes with a unique salt and reviewed, versioned parameters. Plaintext passwords are accepted only at the login boundary or generated by the one-time credential workflow, then discarded. - Passwords are stored only as Argon2id encoded hashes with a unique salt and reviewed, versioned parameters. Plaintext passwords are accepted only at the login boundary or generated by the one-time credential workflow, then discarded.
- An `Authenticated Session` belongs to one merchant user and records creation, last activity, idle expiry, absolute expiry, revocation, and minimal security provenance required by the approved retention policy. - An `Authenticated Session` belongs to one merchant user and records creation, last activity, idle expiry, absolute expiry, revocation, and minimal security provenance required by the approved retention policy.
- The browser receives only a cryptographically random opaque session token. The server stores a non-reversible digest of that token, never the raw token. - The browser receives only a cryptographically random opaque session token. The server stores a non-reversible digest of that token, never the raw token.
@@ -58,6 +60,9 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
5. Invalid username, invalid password, disabled account, and other expected login failures return the same user-facing message and externally equivalent response shape. 5. Invalid username, invalid password, disabled account, and other expected login failures return the same user-facing message and externally equivalent response shape.
6. Authentication and authorization failures never reveal password hashes, raw tokens, user existence, merchant existence, stack traces, or internal policy details. 6. Authentication and authorization failures never reveal password hashes, raw tokens, user existence, merchant existence, stack traces, or internal policy details.
7. Protected HTTP and SSE requests use the same authenticated merchant boundary. SSE replay or reconnect never survives session expiry or revocation. 7. Protected HTTP and SSE requests use the same authenticated merchant boundary. SSE replay or reconnect never survives session expiry or revocation.
8. State-changing authenticated requests require a valid CSRF token and same-origin evidence. `SameSite=Lax` is defense in depth, not the only CSRF control; unexpected `Origin`/Fetch Metadata requests fail closed.
9. Each user may have at most two active sessions. Creating a third session atomically revokes the oldest active session.
10. Failed logins are throttled by both normalized username and source IP without permanently locking the account. A limited request returns `429` with `Retry-After`; exact thresholds remain configurable and require approval.
## Demo credential workflow ## Demo credential workflow
@@ -72,8 +77,9 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
| Boundary/asset | Main abuse case | Required control | | Boundary/asset | Main abuse case | Required control |
|---|---|---| |---|---|---|
| Login endpoint / credentials | Enumeration or credential guessing | Generic failures; login throttling policy still requires approval | | Login endpoint / credentials | Enumeration or credential guessing | Generic failures; username and IP throttling; no permanent account lockout |
| Browser / session token | Token theft through script or transport | `HttpOnly`, `Secure`, host-only cookie; HTTPS; no browser storage | | Browser / session token | Token theft through script or transport | `HttpOnly`, `Secure`, host-only cookie; HTTPS; no browser storage |
| Authenticated mutation / session cookie | Cross-site request forgery | CSRF token, strict origin/fetch-metadata checks, `SameSite=Lax` |
| Session store | Stolen database reveals usable tokens | Store only token digests; expire and revoke server-side | | Session store | Stolen database reveals usable tokens | Store only token digests; expire and revoke server-side |
| Merchant API / tenant data | User changes a merchant id to read another tenant | Derive authorization scope from session on every operation | | Merchant API / tenant data | User changes a merchant id to read another tenant | Derive authorization scope from session on every operation |
| Demo credential generation | Plaintext secrets leak into Git/logs/history | Random generation, one-time output, explicit secure destination, secret scanning | | Demo credential generation | Plaintext secrets leak into Git/logs/history | Random generation, one-time output, explicit secure destination, secret scanning |
@@ -85,6 +91,7 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- Authenticated users requesting a resource outside their merchant boundary receive a typed forbidden/not-found policy defined by the API spec without confirming another merchant's resource exists. - Authenticated users requesting a resource outside their merchant boundary receive a typed forbidden/not-found policy defined by the API spec without confirming another merchant's resource exists.
- Session-store failure fails closed: protected requests do not proceed using unverifiable client state. - Session-store failure fails closed: protected requests do not proceed using unverifiable client state.
- Credential-generation partial failure reports affected merchant identifiers without printing generated passwords again or leaving an apparently complete run. - Credential-generation partial failure reports affected merchant identifiers without printing generated passwords again or leaving an apparently complete run.
- A throttled login returns a typed `429` with `Retry-After` and does not perform password verification or create a session.
## Observability and audit ## Observability and audit
@@ -103,6 +110,10 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- Invalid username, password, and disabled-account attempts are externally indistinguishable in user-facing content. - Invalid username, password, and disabled-account attempts are externally indistinguishable in user-facing content.
- Modifying a merchant identifier cannot cross the merchant boundary enforced by the authenticated session. - Modifying a merchant identifier cannot cross the merchant boundary enforced by the authenticated session.
- The demo-user workflow creates missing users idempotently, displays new credentials once, and never commits or logs them. - The demo-user workflow creates missing users idempotently, displays new credentials once, and never commits or logs them.
- Generated usernames are stable `merchant-NNNN`-style lowercase identifiers and compare case-insensitively.
- Login throttling applies independently to normalized-username and source-IP buckets, returns `429` plus `Retry-After`, and never permanently locks an account.
- State-changing requests fail without both an approved CSRF token and valid same-origin evidence.
- No user has more than two active sessions; the third successful login revokes the oldest session.
- Security tests cover enumeration resistance, session fixation/rotation, expiry, revocation, cross-tenant access, cookie flags, token-digest storage, and credential-output safety. - Security tests cover enumeration resistance, session fixation/rotation, expiry, revocation, cross-tenant access, cookie flags, token-digest storage, and credential-output safety.
## Boundaries ## Boundaries
@@ -118,13 +129,15 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- Hash passwords with Argon2id and return generic login failures. - Hash passwords with Argon2id and return generic login failures.
- Expire sessions after 12 hours idle and seven days absolute; revoke immediately on logout or account disablement. - Expire sessions after 12 hours idle and seven days absolute; revoke immediately on logout or account disablement.
- Generate one demo user's credentials per merchant through a one-time administrative command, display them once, and keep them out of Git. - Generate one demo user's credentials per merchant through a one-time administrative command, display them once, and keep them out of Git.
- Use stable lowercase `merchant-NNNN`-style usernames with case-insensitive comparison.
- Throttle failed logins by normalized username and source IP, return `429` with `Retry-After`, and do not use permanent account lockouts.
- Require CSRF tokens plus strict same-origin evidence for state-changing authenticated requests.
- Permit at most two active sessions per user; a third successful login revokes the oldest.
- Treat local username/password authentication as a demo adapter that is replaced by ZarinPal SSO after acceptance; keep tenant authorization independent of the credential provider.
## Open decisions ## Open decisions
- Exact username normalization and generated username format - Exact login rate-limit thresholds and time windows
- Login rate-limit keys, thresholds, lockout behavior, and response contract
- CSRF protection for state-changing authenticated requests
- Number of simultaneous sessions permitted per user
- Exact session store, token-digest algorithm, cookie name/path, and activity-write throttling - Exact session store, token-digest algorithm, cookie name/path, and activity-write throttling
- Credential replacement and secure delivery procedure - Credential replacement and secure delivery procedure
- Security-event IP/user-agent collection and retention - Security-event IP/user-agent collection and retention

View File

@@ -5,7 +5,7 @@
## Context ## Context
The merchant dashboard needs simple username/password authentication, strict tenant isolation, logout, account disablement, and authenticated SSE reconnect/replay. Browser-stored JWTs would make immediate revocation and logout enforcement more complex and would expose bearer material to frontend-readable storage if implemented carelessly. A server-side session adds a lookup but keeps authorization state under backend control. The merchant dashboard needs simple username/password authentication for the hackathon demo, strict tenant isolation, logout, account disablement, and authenticated SSE reconnect/replay. Browser-stored JWTs would make immediate revocation and logout enforcement more complex and would expose bearer material to frontend-readable storage if implemented carelessly. A server-side session adds a lookup but keeps authorization state under backend control. If the project is accepted, ZarinPal SSO will replace this demo authentication adapter, so identity work must stay small and separable from merchant authorization.
## Decision ## Decision
@@ -15,10 +15,13 @@ Sessions expire after 12 hours of inactivity and no later than seven days after
Passwords use Argon2id with unique salts and reviewed parameters. Expected login failures are externally generic. Demo credentials are generated through a one-time administrative workflow, displayed once, and never stored in Git or logs. Passwords use Argon2id with unique salts and reviewed parameters. Expected login failures are externally generic. Demo credentials are generated through a one-time administrative workflow, displayed once, and never stored in Git or logs.
Demo usernames are stable lowercase `merchant-NNNN`-style identifiers with case-insensitive comparison. Failed logins are throttled by normalized username and source IP, return `429` with `Retry-After`, and never cause permanent account lockout. State-changing authenticated requests require CSRF tokens and strict same-origin evidence. A user may have at most two active sessions; a third successful login revokes the oldest.
## Consequences ## Consequences
- Logout, disablement, expiry, and incident response can invalidate access immediately. - Logout, disablement, expiry, and incident response can invalidate access immediately.
- The backend session store is on the protected-request path and must fail closed, be indexed, and have observable latency/availability. - The backend session store is on the protected-request path and must fail closed, be indexed, and have observable latency/availability.
- SSE reconnect and replay must reauthorize instead of trusting a previous connection or event id. - SSE reconnect and replay must reauthorize instead of trusting a previous connection or event id.
- Scaling API instances does not require sticky sessions when they share the approved authoritative session store. - Scaling API instances does not require sticky sessions when they share the approved authoritative session store.
- Cookie scope, CSRF controls, login throttling, and the physical session-store contract remain explicit Identity-spec decisions rather than implicit framework defaults. - Cookie scope, login-throttle thresholds, and the physical session-store contract remain explicit Identity-spec decisions rather than implicit framework defaults.
- The credential-verification adapter must be replaceable by ZarinPal SSO without moving tenant authorization into the frontend or analytics packages.