diff --git a/AGENTS.md b/AGENTS.md index 542aab3..bb73589 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 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. -- 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. ## Git and documentation diff --git a/CONTEXT.md b/CONTEXT.md index d8662a6..2f5ca00 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. +## 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 A merchant-owned payment endpoint identified by the source terminal key. diff --git a/docs/IDENTITY-SPEC.md b/docs/IDENTITY-SPEC.md new file mode 100644 index 0000000..2db64d8 --- /dev/null +++ b/docs/IDENTITY-SPEC.md @@ -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 diff --git a/docs/decisions/0014-server-side-merchant-sessions.md b/docs/decisions/0014-server-side-merchant-sessions.md new file mode 100644 index 0000000..1d33d3b --- /dev/null +++ b/docs/decisions/0014-server-side-merchant-sessions.md @@ -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.