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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4e0c90e2cf
commit
a49389dc2c
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user