feat(ADM-018): completed feature
This commit is contained in:
113
legacy/specs-old/F-005-identity/DESIGN.md
Normal file
113
legacy/specs-old/F-005-identity/DESIGN.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# DESIGN — F-005 Identity: register, login, sessions
|
||||
|
||||
## Module layout (hexagonal, screaming)
|
||||
```
|
||||
src/modules/identity/
|
||||
index.ts # public API only: registerIdentityRoutes + types
|
||||
domain/
|
||||
user.ts # User, NewUser types; email normalization
|
||||
session.ts # Session type, token TTL constants
|
||||
errors.ts # InvalidCredentials, EmailAlreadyRegistered, RateLimited
|
||||
ports.ts # PasswordHasher, UserRepository, SessionRepository
|
||||
application/
|
||||
register-user.ts # RegisterUser use case
|
||||
login.ts # Login use case (rate limit + timing equalization)
|
||||
logout.ts # Logout use case (revoke by token hash)
|
||||
login-rate-limiter.ts # LoginRateLimiter interface + InMemoryLoginRateLimiter
|
||||
infrastructure/
|
||||
argon2-password-hasher.ts # argon2id, OWASP params
|
||||
pg-user-repository.ts # identity_users CRUD (pg parameterized)
|
||||
pg-session-repository.ts # identity_sessions create/revoke-by-token-hash
|
||||
session-token.ts # random token + sha256 hash helpers
|
||||
api/
|
||||
identity.routes.ts # POST /auth/register | /auth/login | /auth/logout
|
||||
tests/
|
||||
login-rate-limiter.test.ts # unit
|
||||
session-token.test.ts # unit
|
||||
```
|
||||
|
||||
Rules respected: domain imports nothing from infra/frameworks; application depends on
|
||||
domain ports only; infrastructure implements ports; api is a thin adapter using
|
||||
`parseJson` (F-003) and maps domain errors to AppError.
|
||||
|
||||
## Data model — migration 002_identity
|
||||
```sql
|
||||
identity_users(
|
||||
id uuid PK DEFAULT gen_random_uuid(),
|
||||
email citext NOT NULL UNIQUE,
|
||||
password_hash text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
identity_sessions(
|
||||
id uuid PK DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES identity_users(id) ON DELETE CASCADE,
|
||||
token_hash text NOT NULL UNIQUE, -- sha256 of the opaque cookie token
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz
|
||||
) + index on user_id
|
||||
```
|
||||
- citext for case-insensitive unique email (extension from baseline).
|
||||
- Only the token HASH is stored: a DB leak does not yield usable sessions.
|
||||
- Down drops both tables (reverse FK order).
|
||||
|
||||
## Password hashing
|
||||
- argon2id, memoryCost 19456 KiB, timeCost 2, parallelism 1 (OWASP 2024 baseline).
|
||||
- Output PHC string (`$argon2id$...`) stored as-is; verify via argon2.verify.
|
||||
- Timing equalization: unknown email still runs one verify against a module-level
|
||||
dummy hash so 401 timing does not enumerate users.
|
||||
|
||||
## Sessions and cookie
|
||||
- Token: `crypto.randomBytes(64).toString('base64url')` (opaque, 512 bits).
|
||||
- TTL: 7 days, stamped in `expires_at`; expired/revoked sessions are invalid.
|
||||
- Cookie `mdv_session`: HttpOnly, SameSite=Lax, Path=/, Secure per config
|
||||
(`COOKIE_SECURE`, default true; `.env.example` sets false for local http dev),
|
||||
Max-Age = TTL.
|
||||
- Logout revokes by token hash (`revoked_at`), always answers 204 and clears the
|
||||
cookie (idempotent).
|
||||
|
||||
## Rate limiting
|
||||
```ts
|
||||
interface LoginRateLimiter {
|
||||
consume(key: string): { allowed: true } | { allowed: false; retryAfterMs: number };
|
||||
recordFailure(key: string): void;
|
||||
reset(key: string): void;
|
||||
}
|
||||
```
|
||||
- InMemoryLoginRateLimiter: per normalized email, threshold 10 consecutive failures,
|
||||
15-minute cooldown once reached, reset on success. Lazy sweep of expired entries.
|
||||
- In-memory per instance (documented limitation; interface allows Redis-backed swap).
|
||||
- 429 response includes `Retry-After` seconds.
|
||||
|
||||
## Endpoints
|
||||
| Route | Success | Errors |
|
||||
|---|---|---|
|
||||
| POST /auth/register | 201 `{ id, email, createdAt }` | 400 VALIDATION_ERROR, 409 EMAIL_ALREADY_REGISTERED |
|
||||
| POST /auth/login | 200 `{ id, email }` + Set-Cookie | 401 INVALID_CREDENTIALS (generic), 429 TOO_MANY_ATTEMPTS |
|
||||
| POST /auth/logout | 204 + clear cookie | — (idempotent) |
|
||||
|
||||
Request bodies validated with zod via `parseJson` (F-003 hook): email + password
|
||||
(min 8, max 128 chars).
|
||||
|
||||
## Wiring
|
||||
- `pool.ts`: `createPool(connectionString)` replaces `createPoolFromEnv` (config owns env).
|
||||
- `config.ts`: adds optional `COOKIE_SECURE` (default true).
|
||||
- `buildApp(deps)`: new optional `pool` + `cookieSecure`; when pool present it registers
|
||||
identity routes. No pool = foundation-only app (keeps F-001..F-004 tests DB-free).
|
||||
- `server.ts`: creates pool from config, passes into buildApp.
|
||||
|
||||
## Test strategy
|
||||
- Unit (module-scoped): rate limiter thresholds/reset/cooldown; session token
|
||||
randomness/hashing; domain error mapping.
|
||||
- Composition (app-scoped itest, real PostgreSQL): register/login/logout happy paths;
|
||||
argon2 PHC hash in DB + no plaintext; duplicate email 409; identical 401 bodies for
|
||||
wrong password vs unknown email; cookie attributes; 10 failures -> 429 + Retry-After;
|
||||
logout revokes (second logout idempotent, session unusable).
|
||||
|
||||
## Risks / mitigations
|
||||
- Native argon2 build: toolchain present; if install fails, switch impl behind the
|
||||
PasswordHasher port to @node-rs/argon2 (no design change).
|
||||
- Enumeration: generic 401 + dummy verify; register 409 accepted as standard contract
|
||||
(login is the enumeration-sensitive path per acceptance).
|
||||
- In-memory rate limiter bypass via multi-instance: documented; interface ready for Redis.
|
||||
37
legacy/specs-old/F-005-identity/SPEC.md
Normal file
37
legacy/specs-old/F-005-identity/SPEC.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# SPEC — F-005 Identity: register, login, sessions
|
||||
|
||||
## Problem
|
||||
Customers need accounts; nothing trusts who calls the API.
|
||||
|
||||
## Goal
|
||||
Registration, login, logout with hashed passwords and secure sessions — server is the
|
||||
only authority for identity (never trust the frontend).
|
||||
|
||||
## Scope IN
|
||||
- `src/modules/identity` with hexagonal layers: domain / application / infrastructure / api
|
||||
- Register, login, logout use cases over PostgreSQL (source of truth)
|
||||
- Argon2id password hashing (OWASP parameters)
|
||||
- Server-side opaque sessions stored in DB; cookie carries a random token, DB stores only its SHA-256 hash
|
||||
- Secure session cookie: HttpOnly, Secure, SameSite
|
||||
- Login rate limiting (10 consecutive failures -> 429 with cooldown) behind an interface
|
||||
- Migration 002_identity (identity_users, identity_sessions) with down
|
||||
- Config: COOKIE_SECURE flag (default true)
|
||||
|
||||
## Scope OUT
|
||||
- No MFA, no OAuth providers, no profile editing
|
||||
- No email verification flow, no password reset
|
||||
- No distributed rate limiting (in-memory per instance; documented)
|
||||
- No expired-session sweeper (follow-up)
|
||||
|
||||
## Acceptance criteria
|
||||
1. Given valid credentials When login Then HTTP 200 and secure session cookie set.
|
||||
2. Given wrong password When login Then HTTP 401 and no user enumeration hint (unknown email returns the identical 401).
|
||||
3. Passwords stored with argon2, never plaintext or reversible.
|
||||
4. Given 10 failed logins in a row When next login attempted Then HTTP 429.
|
||||
5. Session cookie is HttpOnly, Secure and SameSite.
|
||||
6. `./scripts/verify.sh` green.
|
||||
|
||||
## Dependencies added
|
||||
- argon2 (canonical Argon2id implementation; PHC string output)
|
||||
- @fastify/cookie (explicit setCookie/clearCookie; official Fastify plugin)
|
||||
Justification goes to `spec/tech.md`.
|
||||
13
legacy/specs-old/F-005-identity/TASKS.md
Normal file
13
legacy/specs-old/F-005-identity/TASKS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# TASKS — F-005 Identity: register, login, sessions
|
||||
|
||||
- [ ] TASK-001 deps: argon2 + @fastify/cookie; justify in spec/tech.md
|
||||
- [ ] TASK-002 migrations/002_identity.js (identity_users + identity_sessions, reversible)
|
||||
- [ ] TASK-003 domain: user.ts, session.ts, errors.ts, ports.ts
|
||||
- [ ] TASK-004 application: register-user.ts, login.ts, logout.ts, login-rate-limiter.ts
|
||||
- [ ] TASK-005 infrastructure: argon2 hasher, pg repos, session-token
|
||||
- [ ] TASK-006 api: identity.routes.ts (zod via parseJson, error mapping, cookie)
|
||||
- [ ] TASK-007 index.ts public API
|
||||
- [ ] TASK-008 foundation wiring: pool.createPool(connectionString), config COOKIE_SECURE, buildApp(pool, cookieSecure), server.ts pool creation
|
||||
- [ ] TASK-009 unit tests: rate limiter + session token (module-scoped)
|
||||
- [ ] TASK-010 integration tests: full flows against real PostgreSQL (app-scoped itest)
|
||||
- [ ] TASK-011 run lint + boundaries + typecheck + build + test + test:integration + verify.sh
|
||||
25
legacy/specs-old/F-005-identity/TESTS.md
Normal file
25
legacy/specs-old/F-005-identity/TESTS.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# TESTS — F-005 Identity: register, login, sessions
|
||||
|
||||
## Unit — src/modules/identity/tests/login-rate-limiter.test.ts
|
||||
1. Allows attempts below threshold; records failures
|
||||
2. Blocks with retryAfterMs after 10 consecutive failures (AC4 logic)
|
||||
3. Success resets the counter
|
||||
4. Cooldown expiry re-allows attempts
|
||||
|
||||
## Unit — src/modules/identity/tests/session-token.test.ts
|
||||
1. Tokens are unique and URL-safe
|
||||
2. hashSessionToken is deterministic sha256 hex and never equals the raw token
|
||||
|
||||
## Integration — src/app/tests/identity.itest.ts (real PostgreSQL, fresh DB per run)
|
||||
1. register: 201 + body {id, email, createdAt}; DB row stores $argon2id$ hash; plaintext password appears nowhere in the row (AC3)
|
||||
2. register duplicate email: 409 EMAIL_ALREADY_REGISTERED
|
||||
3. register invalid payload: 400 VALIDATION_ERROR
|
||||
4. login valid: 200 + mdv_session cookie with HttpOnly; SameSite=Lax; Secure; Max-Age (AC1, AC5)
|
||||
5. login wrong password: 401 INVALID_CREDENTIALS; unknown email returns byte-identical error body (AC2)
|
||||
6. rate limit: 10 failed logins then next attempt -> 429 TOO_MANY_ATTEMPTS + Retry-After header (AC4)
|
||||
7. logout: 204 + cookie cleared; session row revoked; second logout still 204 (idempotent)
|
||||
8. buildApp without pool: /auth/login is 404 (foundation-only app preserved)
|
||||
|
||||
## QA live
|
||||
- register/login/logout over HTTP with curl; cookie flags visible in Set-Cookie
|
||||
- verify.sh green (AC6)
|
||||
Reference in New Issue
Block a user