3.3 KiB
ADR 0014: Use revocable server-side merchant sessions
- Status: Accepted
- Date: 2026-08-20
Context
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
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 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.
- The credential-verification adapter must be replaceable by ZarinPal SSO without moving tenant authorization into the frontend or analytics packages.