docs: finalize demo session persistence

This commit is contained in:
2026-08-20 17:21:25 +03:30
parent 384ea54282
commit 50030a20e8
3 changed files with 22 additions and 11 deletions

View File

@@ -47,8 +47,8 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- A `Merchant User` belongs to exactly one merchant in the MVP.
- 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.
- 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.
- An `Authenticated Session` is stored in PostgreSQL, belongs to one merchant user, and records creation, last activity, idle expiry, absolute expiry, and revocation.
- The browser receives a 32-byte cryptographically random opaque session token. PostgreSQL stores only its SHA-256 digest, never the raw token. A fast digest is appropriate because the generated token has high entropy; password-style slow hashing adds cost without protecting a guessable secret.
- Every merchant-scoped backend operation resolves merchant identity from the authenticated session. Client-supplied merchant identifiers may filter only within that boundary and never grant access.
## Authentication and session contract
@@ -62,7 +62,9 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
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.
10. Failed logins are throttled without permanent lockout: five failures per five minutes for a normalized username and 30 failures per five minutes for a source IP. A limited request returns `429` with `Retry-After`; thresholds remain validated configuration with these defaults.
11. The production cookie is named `__Host-zpa_session`, has path `/`, and has no `Domain` attribute. This preserves host-only behavior required by the `__Host-` prefix.
12. Session activity is persisted at most once every five minutes. This keeps the 12-hour idle deadline sufficiently accurate without turning every dashboard/API/SSE interaction into a PostgreSQL write.
## Demo credential workflow
@@ -72,6 +74,7 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
4. New plaintext credentials are displayed or exported once through an explicitly selected secure destination, with restrictive filesystem permissions where a file is requested.
5. The workflow never writes plaintext credentials to application logs, import reports, source-controlled files, shell history by default, or database columns.
6. Lost demo credentials are replaced only through a separate explicit operator action whose revocation and audit behavior must be specified before implementation.
7. The replacement action sets a new random password, revokes both active sessions, and outputs the new credential once to an explicit operator-selected destination outside the repository.
## Threat model and abuse cases
@@ -97,7 +100,8 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- Record successful login, failed login category without username-existence disclosure, logout, session revocation, account disablement, and credential-generation operator actions.
- Logs and metrics exclude passwords, password hashes, raw session tokens, cookie values, and sensitive payment data.
- Security-event retention, IP/user-agent treatment, and privacy minimization remain open decisions.
- Do not persist raw IP addresses or user-agent strings in application audit records for the demo. Rate-limit buckets may use keyed, non-reversible IP representations in disposable Redis entries that expire with the configured window.
- Retain the minimal demo authentication audit records for 30 days, then delete them. This policy is replaced by ZarinPal's production SSO/audit requirements after acceptance.
## Acceptance criteria
@@ -114,6 +118,10 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- 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.
- PostgreSQL contains only SHA-256 digests of 32-byte random session tokens; raw tokens never enter server storage or logs.
- Session activity writes occur no more than once per five-minute window while idle expiry remains enforceable.
- Production cookies use `__Host-zpa_session`, path `/`, no `Domain`, `Secure`, `HttpOnly`, and `SameSite=Lax`.
- Minimal demo authentication audit records exclude raw IP/user-agent values and expire after 30 days.
- Security tests cover enumeration resistance, session fixation/rotation, expiry, revocation, cross-tenant access, cookie flags, token-digest storage, and credential-output safety.
## Boundaries
@@ -134,10 +142,10 @@ Allow hackathon judges and merchants to authenticate as distinct merchant users,
- 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.
- Store session rows in PostgreSQL, identify 32-byte random tokens by SHA-256 digest, and coalesce activity writes to at most once every five minutes.
- Use the host-only `__Host-zpa_session` cookie name and retain minimal demo authentication audit records for 30 days without raw IP/user-agent values.
- Default login throttling to five failures per normalized username and 30 per source IP in five minutes, without permanent lockout.
## Open decisions
- Exact login rate-limit thresholds and time windows
- Exact session store, token-digest algorithm, cookie name/path, and activity-write throttling
- Credential replacement and secure delivery procedure
- Security-event IP/user-agent collection and retention
No stakeholder-level Identity decisions remain. Implementation parameters may change only within the approved demo boundary and security invariants; ZarinPal SSO requirements require a new capability revision after acceptance.

View File

@@ -36,7 +36,7 @@ Switch response codes remain nullable text on attempts until an authoritative de
| `merchants` | Merchant profile and category membership | unique source merchant key; FK category |
| `terminals` | Merchant-owned payment terminals | unique source terminal key; FK merchant |
| `merchant_users` | Login identities | unique normalized username; FK merchant; password hash only |
| `auth_sessions` | Revocable authenticated sessions | hashed opaque session token; FK user; expiry/revocation |
| `auth_sessions` | Revocable demo authenticated sessions | unique SHA-256 digest of a 32-byte opaque token; FK user; last activity; idle/absolute expiry; revocation |
Synthetic profile fields carry an explicit `profile_is_synthetic` marker. Credentials are generated outside Git.

View File

@@ -9,18 +9,21 @@ The merchant dashboard needs simple username/password authentication for the hac
## Decision
Use cryptographically random opaque session tokens transported only in host-only `Secure`, `HttpOnly`, `SameSite=Lax` cookies. Store only a non-reversible token digest with server-side session state. Never place authentication tokens in local storage, session storage, URLs, or frontend-readable application state.
Use 32-byte cryptographically random opaque session tokens transported only in a host-only `__Host-zpa_session` cookie with path `/`, no `Domain`, `Secure`, `HttpOnly`, and `SameSite=Lax`. Store sessions in PostgreSQL and persist only the SHA-256 token digest. Never place authentication tokens in local storage, session storage, URLs, frontend-readable application state, logs, or database columns.
Sessions expire after 12 hours of inactivity and no later than seven days after creation. Logout revokes the current session before clearing the cookie; disabling a merchant user revokes every active session for that user. Every protected HTTP request, SSE connection, and replay request resolves the merchant boundary from the current server-side session.
Persist activity at most once every five minutes. The idle timeout does not require per-request write precision, and coalescing avoids turning normal dashboard and SSE traffic into unnecessary PostgreSQL writes.
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.
Demo usernames are stable lowercase `merchant-NNNN`-style identifiers with case-insensitive comparison. Failed logins default to five attempts per normalized username and 30 per source IP in five minutes, 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
- 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.
- A PostgreSQL outage also prevents authentication checks; this is preferable to trusting unverifiable client state, and no additional auth service is introduced for the demo.
- 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.
- Cookie scope, login-throttle thresholds, and the physical session-store contract remain explicit Identity-spec decisions rather than implicit framework defaults.