docs: define merchant session baseline

This commit is contained in:
2026-08-20 17:06:43 +03:30
parent d579c2525d
commit 406cd9c0cf
4 changed files with 159 additions and 1 deletions

View File

@@ -30,7 +30,7 @@ For every Go-related task, always invoke and follow the `golang-how-to` skill, t
- Use parameterized queries and transactions with explicit boundaries. Stream/bulk-load the multi-million-row input instead of holding it in memory. - Use parameterized queries and transactions with explicit boundaries. Stream/bulk-load the multi-million-row input instead of holding it in memory.
- Use Ent for application persistence and relationship modeling. Use pgx with PostgreSQL `COPY` for the bulk-import staging path; do not force high-volume ingestion through row-by-row ORM operations. - Use Ent for application persistence and relationship modeling. Use pgx with PostgreSQL `COPY` for the bulk-import staging path; do not force high-volume ingestion through row-by-row ORM operations.
- Database migrations are explicit, versioned, reviewed artifacts applied by a dedicated migration/deployment step. Never run Ent automatic schema creation against production during application startup. - Database migrations are explicit, versioned, reviewed artifacts applied by a dedicated migration/deployment step. Never run Ent automatic schema creation against production during application startup.
- Passwords use an approved adaptive hash (Argon2id or bcrypt per the approved auth spec); sessions use secure, HttpOnly cookies and server-side revocation. - Passwords use Argon2id with reviewed parameters and a unique salt. Sessions are server-side and revocable; browsers receive only an opaque token in a `Secure`, `HttpOnly`, `SameSite=Lax` cookie. Never put authentication tokens in browser storage.
- Required verification includes unit/integration tests, race detection where applicable, `go vet`, `golangci-lint`, `govulncheck`, and migration/import tests. - Required verification includes unit/integration tests, race detection where applicable, `go vet`, `golangci-lint`, `govulncheck`, and migration/import tests.
## Git and documentation ## Git and documentation

View File

@@ -8,6 +8,10 @@ A business represented in the source data. A merchant owns terminals, payment se
A person/account permitted to authenticate as one merchant. The hackathon seed creates an initial demo user for each merchant; a merchant and a merchant user are not the same concept. A person/account permitted to authenticate as one merchant. The hackathon seed creates an initial demo user for each merchant; a merchant and a merchant user are not the same concept.
## Authenticated Session
A time-bounded and revocable authorization grant connecting one authenticated merchant user to exactly one merchant boundary. Logout, account disablement, idle expiry, or absolute expiry ends the grant.
## Terminal ## Terminal
A merchant-owned payment endpoint identified by the source terminal key. A merchant-owned payment endpoint identified by the source terminal key.

130
docs/IDENTITY-SPEC.md Normal file
View File

@@ -0,0 +1,130 @@
# 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 without adding self-service account management to the MVP.
## 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
## Domain and data contract
- A `Merchant User` belongs to exactly one merchant in the MVP.
- Usernames are unique after the approved normalization rule; the exact normalization remains open.
- 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.
- 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.
## 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.
## Threat model and abuse cases
| Boundary/asset | Main abuse case | Required control |
|---|---|---|
| Login endpoint / credentials | Enumeration or credential guessing | Generic failures; login throttling policy still requires approval |
| Browser / session token | Token theft through script or transport | `HttpOnly`, `Secure`, host-only cookie; HTTPS; no browser storage |
| 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.
## 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=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.
- 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.
## Open decisions
- Exact username normalization and generated username format
- 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
- Credential replacement and secure delivery procedure
- Security-event IP/user-agent collection and retention

View File

@@ -0,0 +1,24 @@
# ADR 0014: Use revocable server-side merchant sessions
- Status: Accepted
- Date: 2026-08-20
## 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.
## 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.
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.
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.
## 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.
- 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, CSRF controls, login throttling, and the physical session-store contract remain explicit Identity-spec decisions rather than implicit framework defaults.