Files
support_backend/specs/010-identity-auth/spec.md
T
saqib mirandClaude Sonnet 5 a49389dc2c 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 <noreply@anthropic.com>
2026-09-07 11:09:46 +05:30

13 KiB

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.