# Identity and Access Capability Spec - Status: Approved specification - Implementation authorization: Not granted - Capability: Identity and access - Depends on: Approved Data Foundation specification - Last updated: 2026-08-20 ## 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. 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 - Demo merchant user: authenticates with assigned credentials and receives access to exactly one merchant. - Merchant user: the same MVP authorization model as a demo user; roles and delegated access are not yet defined. - Operator: runs the one-time credential-generation workflow and distributes credentials outside Git. - Unauthenticated visitor: may access only the login flow and public health/static resources explicitly allowlisted by later API/deployment specs. ## In scope - Username/password authentication for one seeded user per merchant - Argon2id password hashing - Server-side, revocable authenticated sessions - Secure cookie transport and logout - Idle and absolute session expiry - Account disablement and session revocation - Generic authentication failures - Tenant identity derived exclusively from the authenticated session - One-time demo-credential generation and secure one-time display - Security-event audit records that contain no credentials or raw session tokens ## Out of scope for MVP - Self-registration - Password reset/recovery - Email or phone verification - Multi-factor authentication - Social login or SSO - User-created roles, permissions, or additional merchant users - Cross-merchant operator access in the browser - 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 - 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` 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 1. Successful login rotates into a new server-side session and sets one cookie with `Secure`, `HttpOnly`, and `SameSite=Lax` attributes. Authentication tokens are never placed in `localStorage`, `sessionStorage`, URLs, or frontend-readable state. 2. The cookie is host-only by default, uses the narrowest practical path, and is transmitted only over HTTPS in deployed environments. Local development behavior must be explicit without weakening production defaults. 3. A session expires after 12 hours without approved activity and always expires seven days after creation. Activity cannot extend the absolute deadline. 4. Logout revokes the server-side session before clearing the browser cookie. Account disablement revokes all active sessions for that user immediately. 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. 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 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 1. A one-time administrative command creates exactly one initial demo user for each merchant that does not already have one. 2. Usernames and random passwords are generated independently from source merchant keys and payment data. 3. Re-running the workflow is idempotent: it does not silently replace existing credentials or create duplicates. 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 | Boundary/asset | Main abuse case | Required control | |---|---|---| | 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 | | 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 | | 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 | | SSE reconnect/replay | Revoked user continues receiving events | Reauthorize every connection and replay request | ## Failure behavior - Missing, malformed, expired, or revoked sessions receive the same typed unauthenticated response and trigger no merchant data access. - 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. - 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 - 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. - 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 - At least two seeded merchant users can authenticate and each sees only its own merchant data. - Password database values are Argon2id hashes; plaintext credentials are absent from database, logs, Git, and normal command history. - The browser contains only a `Secure`, `HttpOnly`, `SameSite=Lax` opaque session cookie and no authentication token in frontend-readable storage. - Raw session tokens are absent from server storage; only non-reversible digests are persisted. - Idle sessions expire after 12 hours, all sessions expire within seven days, and activity never extends the absolute limit. - Logout and account disablement make an existing cookie unusable immediately, including for SSE reconnect/replay. - 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. - 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. - 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 - Always: validate inputs, use parameterized persistence, rotate sessions on login, authorize every protected operation, fail closed, and audit security-relevant state changes without secrets. - Ask first: change session lifetime, cookie scope, login throttling, username normalization, credential delivery, roles, recovery, or personal-data retention. - Never: store plaintext passwords/raw session tokens, trust a client merchant id for authorization, put auth tokens in browser storage or URLs, reveal account existence, or commit credentials. ## Resolved decisions - Use revocable server-side sessions rather than browser-stored JWT authentication. See ADR 0014. - Transport only an opaque token in a `Secure`, `HttpOnly`, `SameSite=Lax` cookie. - 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. - 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. - 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 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.