12 KiB
12 KiB
Identity and Access Capability Spec
- Status: Draft — not approved for implementation
- 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 Userbelongs 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 Sessionbelongs 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.
- 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
- Successful login rotates into a new server-side session and sets one cookie with
Secure,HttpOnly, andSameSite=Laxattributes. Authentication tokens are never placed inlocalStorage,sessionStorage, URLs, or frontend-readable state. - 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.
- A session expires after 12 hours without approved activity and always expires seven days after creation. Activity cannot extend the absolute deadline.
- Logout revokes the server-side session before clearing the browser cookie. Account disablement revokes all active sessions for that user immediately.
- Invalid username, invalid password, disabled account, and other expected login failures return the same user-facing message and externally equivalent response shape.
- Authentication and authorization failures never reveal password hashes, raw tokens, user existence, merchant existence, stack traces, or internal policy details.
- Protected HTTP and SSE requests use the same authenticated merchant boundary. SSE replay or reconnect never survives session expiry or revocation.
- State-changing authenticated requests require a valid CSRF token and same-origin evidence.
SameSite=Laxis defense in depth, not the only CSRF control; unexpectedOrigin/Fetch Metadata requests fail closed. - Each user may have at most two active sessions. Creating a third session atomically revokes the oldest active session.
- Failed logins are throttled by both normalized username and source IP without permanently locking the account. A limited request returns
429withRetry-After; exact thresholds remain configurable and require approval.
Demo credential workflow
- A one-time administrative command creates exactly one initial demo user for each merchant that does not already have one.
- Usernames and random passwords are generated independently from source merchant keys and payment data.
- Re-running the workflow is idempotent: it does not silently replace existing credentials or create duplicates.
- New plaintext credentials are displayed or exported once through an explicitly selected secure destination, with restrictive filesystem permissions where a file is requested.
- The workflow never writes plaintext credentials to application logs, import reports, source-controlled files, shell history by default, or database columns.
- Lost demo credentials are replaced only through a separate explicit operator action whose revocation and audit behavior must be specified before implementation.
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
429withRetry-Afterand 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.
- Security-event retention, IP/user-agent treatment, and privacy minimization remain open decisions.
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=Laxopaque 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
429plusRetry-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.
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=Laxcookie. - 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
429withRetry-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
- 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