From a49389dc2c9636efad595bea33dd5f7d652f1d47 Mon Sep 17 00:00:00 2001 From: saqib mir Date: Mon, 7 Sep 2026 11:09:46 +0530 Subject: [PATCH] docs: spec for identity and authentication (010) Not on the original roadmap -- surfaced as a genuine blocking gap while planning supporthub-web's own agent/admin UI feature: fastify.authenticate has been a complete no-op stub since 002, and identity/auth's login endpoint has never taken a password. User/UserRole (two seeded-but- passwordless demo accounts) and the AuthUser/JwtPayload types were all already scaffolded and clearly intended for exactly this -- this finishes that original wiring rather than inventing a new design. Scope: real login (password hash + JWT), fastify.authenticate actually rejecting invalid sessions, role-based route gating, a self-identity endpoint, admin-created accounts, and logout. Password reset, MFA, and login rate-limiting are explicitly deferred to Phase 11's own security hardening pass. Co-Authored-By: Claude Sonnet 5 --- .../checklists/requirements.md | 48 ++++ specs/010-identity-auth/spec.md | 237 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 specs/010-identity-auth/checklists/requirements.md create mode 100644 specs/010-identity-auth/spec.md diff --git a/specs/010-identity-auth/checklists/requirements.md b/specs/010-identity-auth/checklists/requirements.md new file mode 100644 index 0000000..aa37b02 --- /dev/null +++ b/specs/010-identity-auth/checklists/requirements.md @@ -0,0 +1,48 @@ +# Specification Quality Checklist: Identity and Authentication + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-09-07 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- This feature was not on the original 11-phase roadmap — it surfaced as a genuine blocking gap + while planning supporthub-web's `001-agent-admin-ui`: `fastify.authenticate` has been a + complete no-op stub since 002, and `identity/auth`'s login endpoint has never taken a + password. Numbered 010 in supporthub-api's own sequence since it's a real, immediately-needed + backend prerequisite, not deferred hardening. +- `User`/`UserRole` (with two seeded-but-passwordless demo accounts, + `admin@supporthub.internal`/`agent@supporthub.internal`) and the `AuthUser`/`JwtPayload` + types in `src/common/types` were all found already scaffolded, unwired, and clearly intended + for exactly this feature since the original pre-speckit scaffold — this is a "finish the + originally-intended wiring" feature, not a new design invented from nothing. +- Scope is deliberately narrow: real login + real route gating + role checks + a self-identity + endpoint + admin-created accounts + logout. Password reset, MFA, rate-limiting, and + registration are explicitly out of scope (Assumptions), matching Phase 11's own "security + hardening pass" as the more appropriate later home for those. +- All items pass; no revision iterations were needed. diff --git a/specs/010-identity-auth/spec.md b/specs/010-identity-auth/spec.md new file mode 100644 index 0000000..4d66c62 --- /dev/null +++ b/specs/010-identity-auth/spec.md @@ -0,0 +1,237 @@ +# Feature Specification: Identity and Authentication + +**Feature Branch**: `010-identity-auth` + +**Created**: 2026-09-07 + +**Status**: Draft + +**Input**: User description: "supporthub-web's admin/agent role-gating (its own Phase 1, +001-agent-admin-ui) has no real backend to build on: `fastify.authenticate` is a complete +no-op stub, and the existing `identity/auth` scaffold's login endpoint accepts an email alone +with no password and returns the raw user record, never a session. Build minimal, real +agent/admin authentication in supporthub-api first, as a prerequisite for the frontend feature." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - An agent or admin logs in and receives a session (Priority: P1) + +A user with a SupportHub-issued account (never a SaaS-delegated identity — this is SupportHub's +own staff, per Constitution Principle I's "support org structure" being SupportHub's own +authority) logs in with their email and password and receives a session token that authorizes +their subsequent requests. + +**Why this priority**: Every other story in this feature, and the entire admin/agent-facing +half of supporthub-web, has nothing to build on without this. + +**Independent Test**: Log in with a seeded account's correct credentials; confirm a session +token is returned and a subsequent authenticated request using it succeeds. + +**Acceptance Scenarios**: + +1. **Given** a user account with a set password, **When** they submit the correct email and + password, **Then** they receive a session token and their own `id`/`email`/`name`/`role`. +2. **Given** a user account, **When** they submit an incorrect password, **Then** the request + is rejected with no session token issued — the rejection message MUST NOT reveal whether the + email itself was valid (never "wrong password" vs "no such user" as distinguishable + responses). +3. **Given** no account exists for a submitted email, **When** login is attempted, **Then** it + is rejected with the same indistinguishable-from-wrong-password response as Scenario 2. + +--- + +### User Story 2 - Protected routes require a valid session; admin-only routes require the admin role (Priority: P1) + +Every existing `/admin/*` route (and any other route already gated by the `fastify.authenticate` +stub across features 002-009) actually rejects a request with no valid session, and every route +that should be admin-only actually rejects a valid session whose role isn't `ADMIN`. + +**Why this priority**: This is the entire point of the feature — without it, User Story 1 +issues a token that nothing on the backend actually checks, which is no better than the current +no-op stub. + +**Independent Test**: Call an existing admin route (e.g. creating a team) with no +`Authorization` header, with an expired/malformed token, with a valid agent-role token, and +with a valid admin-role token; confirm exactly the last one succeeds. + +**Acceptance Scenarios**: + +1. **Given** a request with no `Authorization` header, **When** it hits a route gated by + `fastify.authenticate`, **Then** it's rejected as unauthorized. +2. **Given** a request with a malformed, expired, or tampered token, **When** it hits a gated + route, **Then** it's rejected as unauthorized — never silently treated as anonymous/no-op the + way the current stub does. +3. **Given** a valid session for a user whose role is `AGENT`, **When** it hits a route that + requires the `ADMIN` role specifically, **Then** it's rejected as forbidden, distinct from + the unauthorized case above. +4. **Given** a valid session for a user whose role is `ADMIN`, **When** it hits any route gated + by either `fastify.authenticate` or an admin-only requirement, **Then** it succeeds. + +--- + +### User Story 3 - An authenticated user can identify themselves (Priority: P2) + +A logged-in user can ask "who am I" and get back their own identity and role, without needing +to decode their own session token client-side. + +**Why this priority**: Depends on User Story 1. supporthub-web's role-gating (rendering the +admin portal only for admins) needs a reliable way to know the current session's role after +the token is already held — decoding a JWT's claims client-side is a reasonable fallback, but a +real endpoint is what lets that identity be revalidated against current server-side state (e.g. +a deactivated account) rather than trusting a possibly-stale token's own claims forever. + +**Independent Test**: Log in, then call the "who am I" endpoint with the resulting session; +confirm it returns the same identity and role as the login response, and that it's rejected +under the same conditions as User Story 2. + +**Acceptance Scenarios**: + +1. **Given** a valid session, **When** the identity endpoint is called, **Then** it returns the + current `id`/`email`/`name`/`role` for that session. +2. **Given** a session for an account that has since been deactivated, **When** the identity + endpoint (or any gated route) is called, **Then** it's rejected — a session's validity is + re-checked against current account state, not just the token's own unexpired signature. + +--- + +### User Story 4 - An admin creates additional agent/admin accounts (Priority: P2) + +An admin creates a new user account (agent or admin role) with an initial password, since there +is no public self-signup for SupportHub's own staff accounts. + +**Why this priority**: Depends on User Story 2 (admin-only gating). Without this, the only way +to add a second real account is a direct database write — fine for the one seeded bootstrap +admin, not for onboarding a real team. + +**Independent Test**: As an admin, create a new agent account with a password; confirm the new +account can immediately log in (User Story 1) with those credentials. + +**Acceptance Scenarios**: + +1. **Given** an authenticated admin, **When** they create a new account with an email, + name, role, and initial password, **Then** it's created and can log in immediately. +2. **Given** a non-admin session, **When** they attempt to create an account, **Then** it's + rejected as forbidden (User Story 2's own guarantee, exercised here specifically). +3. **Given** an email already in use by an existing account, **When** account creation is + attempted, **Then** it's rejected — never a second account silently sharing one email. + +--- + +### User Story 5 - A user logs out (Priority: P3) + +A logged-in user can end their own session explicitly, rather than only ever waiting for it to +expire. + +**Why this priority**: Lowest priority — a short-lived token that simply expires already +bounds the exposure of a lost/leftover session; an explicit logout is a UX nicety layered on +top, not a security-critical gap the way User Stories 1-2 are. + +**Independent Test**: Log in, log out, then attempt to use the same token again; confirm it's +now rejected. + +**Acceptance Scenarios**: + +1. **Given** a valid session, **When** the user logs out, **Then** that specific token is + rejected on any subsequent use, even though it hasn't yet expired. + +--- + +### Edge Cases + +- What happens to a session already issued to a user whose password is changed or whose account + is deactivated? Out of scope for this feature to build a full revocation-on-every-write + mechanism (Assumptions) — User Story 3's re-check-on-identity-call is the only server-side + re-validation this feature guarantees; a short token lifetime (Assumptions) bounds the rest. +- What happens if two login attempts for the same account happen concurrently with different + passwords (e.g. a credential-stuffing attempt racing a real login)? Each is evaluated + independently against the stored password hash — no shared mutable state between them, so no + new concurrency concern is introduced. +- What happens to the two demo accounts the seed script already creates + (`admin@supporthub.internal`, `agent@supporthub.internal`) which currently have no password? + This feature MUST give them real, seeded passwords (documented for local/dev use only) so the + existing seed script keeps producing an immediately-usable bootstrap admin — never account + IDs that exist but can never actually log in. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The system MUST let a user log in with email and password, returning a session + token and their own identity (`id`/`email`/`name`/`role`) on success. +- **FR-002**: A login attempt with an incorrect password or an unrecognized email MUST be + rejected with an indistinguishable response — the system MUST NOT reveal whether a submitted + email corresponds to an existing account. +- **FR-003**: Passwords MUST be stored only as a salted hash, never in plaintext or in any + reversible form. +- **FR-004**: `fastify.authenticate` MUST reject a request with a missing, malformed, expired, + or otherwise invalid session token — it MUST NOT pass a request through as anonymous/no-op + the way the current stub does. +- **FR-005**: The system MUST provide a way to require a specific role (at minimum, `ADMIN`) + on a route, distinct from and layered on top of `fastify.authenticate`'s own valid-session + check, returning a distinguishable forbidden (not unauthorized) response when the role + requirement fails. +- **FR-006**: Every existing route currently gated by `fastify.authenticate` (across + 002-009's own admin/read surfaces) MUST continue to work for a valid session and MUST now + actually reject an invalid one — this feature does not change which routes are gated, only + makes the gate real. +- **FR-007**: The system MUST provide an endpoint that returns the current session's own + identity and role, re-validated against current account state (not solely the token's own + claims). +- **FR-008**: The system MUST let an authenticated admin create a new account (email, name, + role, initial password), rejecting a duplicate email. +- **FR-009**: The system MUST let a user invalidate their own current session token before its + natural expiry. +- **FR-010**: The two existing seeded demo accounts MUST be given real, working passwords as + part of this feature, documented as local/development credentials. + +### Key Entities + +- **User**: A SupportHub staff identity — email, name, role (`ADMIN`/`AGENT`), and (new in this + feature) a securely hashed password. Distinct from `Agent` (the routing/skills/team-membership + profile an `AGENT`-role `User` has) and from a SaaS-delegated customer identity, which this + feature does not touch. +- **Session**: The short-lived, server-issued proof that a `User` authenticated successfully, + carrying their `id`, `email`, and `role`; revocable before its natural expiry (User Story 5). + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of requests to a `fastify.authenticate`-gated route with no valid session are + rejected, verified across every module's existing admin routes (002-009), not just this + feature's own new endpoints. +- **SC-002**: 100% of admin-only actions are rejected for a valid non-admin session, verified for + at least one action from each module that has one. +- **SC-003**: 0% of login rejections reveal whether the submitted email corresponds to an + existing account, verified by comparing the exact response for both cases. +- **SC-004**: 100% of passwords are stored only as a hash — verified by inspecting the stored + representation directly, never as a value that could be reversed to the original password. +- **SC-005**: An admin can create a working new account and have it log in successfully within + the same test run, with no manual database step. + +## Assumptions + +- **No password-reset/forgot-password flow is built in this feature** — an admin can create a + new account (User Story 4), but resetting an existing one's forgotten password is out of + scope; the smallest viable fix today is an admin recreating the account or a direct + operational fix, not a self-service flow. +- **Session tokens are short-lived JWTs with a fixed expiry, not a server-side session store per + token** — logout (User Story 5) is implemented via a revocation check (a denylist of + logged-out-early tokens), not full server-side session tracking; this keeps token validation + fast (no DB round trip on every request) while still making explicit logout meaningfully + different from "wait for expiry." The exact expiry duration and revocation mechanism are + research.md decisions, not fixed here. +- **No account self-registration** — every account is created either by the seed script (the + two bootstrap demo accounts) or by an existing admin (User Story 4); there is no public + sign-up endpoint, consistent with these being SupportHub's own staff accounts, never a + SaaS-delegated customer identity. +- **This feature does not add a password-strength policy, MFA, or rate-limiting specifically + for login attempts beyond what 002's existing generic rate-limit infrastructure might already + cover incidentally** — those are real hardening concerns explicitly named in + `docs/10-implementation-roadmap.md`'s own Phase 11 ("security hardening pass"), not this + feature's job to anticipate. +- **The `CUSTOMER` value already defined on `UserRole` is never assigned by this feature** — no + code path in this feature creates a `User` with `role: CUSTOMER`; per Constitution Principle + I, customer identity remains exclusively SaaS-delegated (002's inbound trust boundary), never + a local `User` row. The enum value's continued existence is a pre-existing scaffold detail + this feature doesn't need to remove to stay correct.