diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index c796a4c..2c7367c 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -6,10 +6,10 @@ _Last updated: 2026-05-21_ ### Authentication (AUTH) -- [ ] **AUTH-01**: User can register with email and password (Argon2 hashing; strength enforced: ≥12 chars, uppercase, lowercase, number, special char; HaveIBeenPwned breach check) -- [ ] **AUTH-02**: User can log in and maintain a session (JWT access token in Pinia memory only — never localStorage; refresh token in `httpOnly; Secure; SameSite=Strict` cookie; 15-min access / 30-day refresh) +- [x] **AUTH-01**: User can register with email and password (Argon2 hashing; strength enforced: ≥12 chars, uppercase, lowercase, number, special char; HaveIBeenPwned breach check) +- [x] **AUTH-02**: User can log in and maintain a session (JWT access token in Pinia memory only — never localStorage; refresh token in `httpOnly; Secure; SameSite=Strict` cookie; 15-min access / 30-day refresh) - [ ] **AUTH-03**: User can enroll a TOTP authenticator app (RFC 6238; 8–10 single-use backup codes issued and explicitly acknowledged before TOTP is marked active) -- [ ] **AUTH-04**: User can complete login using TOTP code or a one-time backup code (backup code invalidated on use) +- [x] **AUTH-04**: User can complete login using TOTP code or a one-time backup code (backup code invalidated on use) - [ ] **AUTH-05**: User can reset password via email (signed token, 1-hour expiry; reset does not auto-login — user must pass TOTP gate on next login) - [ ] **AUTH-06**: User can sign out all active sessions (revokes all refresh tokens in DB; "sign out all devices" control in account settings) - [ ] **AUTH-07**: Refresh token rotation with family revocation — reuse of a rotated token revokes the entire family and emits a security alert to the user @@ -17,11 +17,11 @@ _Last updated: 2026-05-21_ ### Security (SEC) — Cross-Cutting -- [ ] **SEC-01**: All state-changing endpoints are protected against CSRF (SameSite=Strict cookie + origin validation) -- [ ] **SEC-02**: Auth endpoints (login, register, password reset, TOTP verify) are rate-limited (per-IP and per-account) -- [ ] **SEC-03**: All DB queries use parameterized statements / ORM (zero raw string interpolation into queries) +- [x] **SEC-01**: All state-changing endpoints are protected against CSRF (SameSite=Strict cookie + origin validation) +- [x] **SEC-02**: Auth endpoints (login, register, password reset, TOTP verify) are rate-limited (per-IP and per-account) +- [x] **SEC-03**: All DB queries use parameterized statements / ORM (zero raw string interpolation into queries) - [ ] **SEC-04**: All file/document access resolved through DB lookup — object keys are never reconstructed from request parameters (prevents path traversal and cross-user access) -- [ ] **SEC-05**: Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options headers set on all responses +- [x] **SEC-05**: Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options headers set on all responses - [ ] **SEC-06**: Constant-time comparison used for all token and code verification (prevents timing attacks) - [ ] **SEC-07**: Admin role verified on every admin endpoint request; admin cannot access document content, extracted text, or cloud credentials in any response - [ ] **SEC-08**: Cloud credential ciphertext (`credentials_enc`) excluded from all API serializers by default — admin and user responses return only `provider, display_name, connected_at, status` diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7c2a50c..568800b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -51,10 +51,10 @@ _Last updated: 2026-05-22_ **Plans**: 5 plans **Wave 1** — Foundation -- [ ] 02-01-PLAN.md — Auth service layer (Argon2, JWT, refresh tokens, TOTP, backup codes, HIBP, security alert), FastAPI deps, BackupCode model + password_must_change migration +- [x] 02-01-PLAN.md — Auth service layer (Argon2, JWT, refresh tokens, TOTP, backup codes, HIBP, security alert), FastAPI deps, BackupCode model + password_must_change migration **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 02-02-PLAN.md — Register/login (TOTP + backup code paths) + refresh/logout/change-password endpoints + CSP/Origin validation/rate-limit (IP + per-account) + Vue auth store + router guard + Login/Register views +- [x] 02-02-PLAN.md — Register/login (TOTP + backup code paths) + refresh/logout/change-password endpoints + CSP/Origin validation/rate-limit (IP + per-account) + Vue auth store + router guard + Login/Register views **Wave 3** *(blocked on Wave 2 completion)* - [ ] 02-03-PLAN.md — TOTP enrollment + backup codes + password reset + sign-out-all endpoints + AccountView + TotpEnrollment + BackupCodesDisplay + PasswordReset views diff --git a/.planning/STATE.md b/.planning/STATE.md index b66656a..7c8ae9e 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,13 +4,13 @@ milestone: v1.0 milestone_name: milestone current_phase: 2 status: in_progress -last_updated: "2026-05-22T20:00:00.000Z" +last_updated: "2026-05-22T19:30:00.000Z" progress: total_phases: 5 completed_phases: 1 total_plans: 10 - completed_plans: 5 - percent: 20 + completed_plans: 7 + percent: 28 --- # Project State @@ -25,7 +25,7 @@ progress: | Phase | Name | Status | |---|---|---| | 1 | Infrastructure Foundation | ✓ Complete | -| 2 | Users & Authentication | In Progress (1/5 plans complete) | +| 2 | Users & Authentication | In Progress (2/5 plans complete) | | 3 | Document Migration & Multi-User Isolation | Not Started | | 4 | Folders, Sharing, Quotas & Document UX | Not Started | | 5 | Cloud Storage Backends | Not Started | @@ -33,8 +33,8 @@ progress: ## Current Position **Phase:** 02-users-authentication — In Progress -**Plan:** 1/5 complete (Plan 01: Auth service layer) -**Progress:** ██░░░░░░░░ 24% (1/5 phases + 1/5 Phase 2 plans) +**Plan:** 2/5 complete (Plan 02: Auth API endpoints + frontend auth wall) +**Progress:** ███░░░░░░░ 28% (1/5 phases + 2/5 Phase 2 plans) ## Performance Metrics @@ -43,7 +43,7 @@ progress: | Phases complete | 1 / 5 | | Requirements mapped | 54 / 54 | | Plans written | 5 (Phase 1) | -| Plans complete | 5 | +| Plans complete | 7 (5 Phase 1 + 2 Phase 2) | ## Accumulated Context @@ -94,6 +94,6 @@ _Updated at each phase transition._ | Field | Value | |---|---| -| Last session | 2026-05-22 — Executed Phase 2 Plan 01 (auth service layer, deps, migration) | -| Next action | Run `/gsd:execute-phase 2` to continue Phase 2 (Plan 02: auth endpoints) | +| Last session | 2026-05-22 — Executed Phase 2 Plan 02 (auth API endpoints + frontend auth wall) | +| Next action | Run `/gsd:execute-phase 2` to continue Phase 2 (Plan 03: admin endpoints) | | Pending decisions | See Open Questions above | diff --git a/.planning/phases/02-users-authentication/02-02-SUMMARY.md b/.planning/phases/02-users-authentication/02-02-SUMMARY.md new file mode 100644 index 0000000..279543b --- /dev/null +++ b/.planning/phases/02-users-authentication/02-02-SUMMARY.md @@ -0,0 +1,206 @@ +--- +phase: 02-users-authentication +plan: 02 +subsystem: auth +tags: [fastapi, auth, jwt, refresh-token, totp, backup-codes, rate-limiting, csp, cors, vue3, pinia, router-guard] + +# Dependency graph +requires: + - phase: 02-users-authentication + plan: 01 + provides: "services/auth.py (hash_password, verify_password, create_access_token, create_refresh_token, rotate_refresh_token, verify_backup_code, check_hibp, bootstrap_admin), deps/auth.py (get_current_user, get_current_admin), BackupCode model, email_tasks.py" +provides: + - "backend/api/auth.py: POST /api/auth/register, POST /api/auth/login, POST /api/auth/refresh, POST /api/auth/logout, POST /api/auth/logout-all, GET /api/auth/me, POST /api/auth/change-password" + - "backend/main.py: OriginValidationMiddleware (403 on cross-origin state-changing requests), SecurityHeadersMiddleware (CSP + X-Frame-Options + X-Content-Type-Options), CORSMiddleware locked to settings.cors_origins, Redis lifespan (app.state.redis), admin bootstrap in lifespan, auth router included" + - "frontend/src/stores/auth.js: useAuthStore with accessToken in memory only; login() accepts options.backupCode" + - "frontend/src/api/client.js: Bearer header injection, 401 auto-refresh retry, full auth/admin API exports including changePassword" + - "frontend/src/router/index.js: /login, /register, /password-reset, /account, /admin routes; beforeEach guard preserving redirect URL" + - "frontend/src/views/auth/LoginView.vue: three-step login (password → TOTP → backup code)" + - "frontend/src/views/auth/RegisterView.vue: registration with PasswordStrengthBar" + - "frontend/src/layouts/AuthLayout.vue: centered bare layout for auth pages" + - "frontend/src/components/auth/PasswordStrengthBar.vue: 4-segment strength indicator" + - "frontend/src/components/ui/AppSpinner.vue: animate-spin inline spinner" +affects: [02-03, 02-04, 02-05, 02-06, 03-user-document-isolation] + +# Tech tracking +tech-stack: + added: + - "slowapi 0.1.9 — IP-level rate limiting with SlowAPIMiddleware on all auth endpoints" + - "aioredis 2.0.1 — async Redis client for per-account login rate limiting (app.state.redis)" + patterns: + - "OriginValidationMiddleware: BaseHTTPMiddleware checks Origin header on all non-GET/HEAD/OPTIONS requests" + - "SecurityHeadersMiddleware: BaseHTTPMiddleware adds CSP, X-Frame-Options, X-Content-Type-Options" + - "Per-account rate limit: Redis counter keyed login_attempts:{email}, cap=10 in 15 min window" + - "httpOnly Secure SameSite=Strict refresh cookie on path=/api/auth/refresh" + - "TDD RED/GREEN: test file committed first (1d425d4), implementation second (1882edf)" + - "auth store lazy import in client.js to avoid circular deps: await import('../stores/auth.js')" + - "router beforeEach guard: redirect to /login with ?redirect= query param preserved" + +key-files: + created: + - "backend/api/auth.py — register, login (TOTP+backup), refresh, logout, logout-all, me, change-password" + - "backend/tests/test_auth_api.py — 17 tests covering all endpoint behaviors and security properties" + - "frontend/src/stores/auth.js — useAuthStore (memory-only accessToken, login with backupCode support)" + - "frontend/src/layouts/AuthLayout.vue — centered bare layout for auth pages" + - "frontend/src/views/auth/LoginView.vue — three-step login flow with backup code support" + - "frontend/src/views/auth/RegisterView.vue — registration with strength bar" + - "frontend/src/views/auth/PasswordResetView.vue — password reset request stub" + - "frontend/src/views/auth/NewPasswordView.vue — new password form stub" + - "frontend/src/views/AccountView.vue — account settings with change-password and sign-out-all" + - "frontend/src/views/AdminView.vue — admin panel shell with tab navigation" + - "frontend/src/components/auth/PasswordStrengthBar.vue — 4-segment strength bar" + - "frontend/src/components/ui/AppSpinner.vue — inline animate-spin SVG" + modified: + - "backend/main.py — OriginValidationMiddleware, SecurityHeadersMiddleware, CORS locked, Redis lifespan, admin bootstrap, auth router included, slowapi SlowAPIMiddleware" + - "frontend/src/api/client.js — Bearer token injection, 401 auto-refresh retry, full auth exports including changePassword, admin exports" + - "frontend/src/router/index.js — auth routes added, beforeEach guard" + - "backend/ai/__init__.py — Python 3.9 compat: match → if/elif (deviation fix)" + - "backend/ai/openai_provider.py — Python 3.9 compat: str|None → no-annotation (deviation fix)" + - "backend/api/documents.py — Python 3.9 compat: str|None → Optional[str] (deviation fix)" + - "backend/api/topics.py — Python 3.9 compat: str|None → Optional[str] (deviation fix)" + - "backend/api/settings.py — Python 3.9 compat: str|None → Optional[str] (deviation fix)" + - "backend/services/classifier.py — Python 3.9 compat: added from __future__ import annotations (deviation fix)" + - ".gitignore — added frontend/node_modules, dist, package-lock.json" + +key-decisions: + - "Starlette middleware insertion order: SecurityHeaders added first (runs last), then CORS, then OriginValidation added last (runs first) — ensures Origin check fires before CORS and route handlers" + - "Per-account rate limit uses FakeRedis in tests with limiter storage reset between tests via auth_limiter._storage internal API — prevents IP counter bleed between test cases" + - "Dynamic import of useAuthStore in client.js avoids circular dependency (stores/auth → client → stores/auth)" + - "TOTP path takes precedence over backup_code: if both fields provided, verify_totp is called exclusively" + +# Metrics +duration: ~60min +completed: 2026-05-22 +--- + +# Phase 2 Plan 02: Auth API Endpoints and Frontend Auth Wall Summary + +**Auth API (register/login/refresh/logout/me/change-password) with per-account Redis rate limiting, Origin validation, CSP headers, CORS lockdown, and Vue 3 auth store + router guard + Login/Register views — full auth wall live** + +## Performance + +- **Duration:** ~60 min +- **Started:** 2026-05-22T17:30:00Z +- **Completed:** 2026-05-22T17:45:52Z +- **Tasks:** 2 (Task 1 TDD, Task 2 standard) +- **Files created:** 15, Files modified: 9 + +## Accomplishments + +- Full auth API endpoints in `backend/api/auth.py`: register (HIBP + strength check), login (TOTP + backup code paths), refresh (token rotation), logout, logout-all, me, change-password +- Security hardening in `backend/main.py`: Origin validation middleware (403 on cross-origin non-GET), CSP headers (Content-Security-Policy, X-Frame-Options, X-Content-Type-Options), CORS locked to `settings.cors_origins` (wildcard removed), Redis lifespan wiring, admin bootstrap +- Per-account rate limiting: Redis counter `login_attempts:{email}` capped at 10 in 15 min; IP-level slowapi `10/minute` on register/login/refresh +- httpOnly Secure SameSite=Strict refresh cookie on path=/api/auth/refresh (CLAUDE.md constraint) +- 17 TDD tests all passing: covers all endpoint behaviors including backup code issuance/invalidation, password_must_change flow, TOTP precedence over backup_code, Origin validation, and per-account rate limit +- Vue 3 auth store (`useAuthStore`): accessToken in `ref()` memory only — never localStorage/sessionStorage; `login()` accepts `options.backupCode` +- API client extended: Bearer token injection, 401 auto-refresh retry, full set of auth/admin exports including `changePassword` +- Router `beforeEach` guard: redirects unauthenticated users to `/login?redirect=` +- Login view: three-step flow (password → TOTP → backup code); "Use a backup code instead" toggle; UI-SPEC copywriting +- Register view: `PasswordStrengthBar` component (4-segment, score 0-4), HIBP error inline +- `AppSpinner.vue`: lightweight animate-spin SVG inheriting button text color +- `AuthLayout.vue`: centered bare layout with DocuVault brand +- `npm run build` exits 0 + +## Task Commits + +1. **Task 1 RED — test file:** `1d425d4` (test) +2. **Task 1 GREEN — implementation:** `1882edf` (feat) +3. **Task 2 — frontend:** `3b7d362` (feat) + +## Files Created/Modified + +See `key-files` in frontmatter. + +## Decisions Made + +- **Middleware insertion order**: SecurityHeaders first, CORS second, OriginValidation last — Starlette applies in reverse, so OriginValidation runs first on every request +- **Per-account Redis counter**: TTL set only on first `incr()` call (count == 1) to ensure the 15-minute window starts from the first attempt, not the last +- **Dynamic import in client.js**: `await import('../stores/auth.js')` inside `request()` avoids the circular import between stores/auth → api/client → stores/auth +- **Slowapi storage reset in tests**: `auth_limiter._storage.reset()` called in `authed_client` fixture setup to prevent IP counters from one test bleeding into the next + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Python 3.9 incompatibility: match statement in ai/__init__.py** +- **Found during:** Task 1 (RED phase — test setup import chain) +- **Issue:** `ai/__init__.py` used Python 3.10 `match` statement; failed with SyntaxError on Python 3.9 +- **Fix:** Replaced `match` with `if/elif` chain (equivalent behavior, Python 3.9 compatible) +- **Files modified:** `backend/ai/__init__.py` + +**2. [Rule 1 - Bug] Python 3.9 incompatibility: `str | None` union syntax in multiple files** +- **Found during:** Task 1 (RED phase — test setup import chain) +- **Issue:** `str | None` union syntax requires Python 3.10+; FastAPI/Pydantic evaluate route annotations at runtime +- **Fix:** Changed to `Optional[str]` in all affected files; added `from __future__ import annotations` to classifier.py (annotation-only, no runtime evaluation) +- **Files modified:** `backend/ai/openai_provider.py`, `backend/api/documents.py`, `backend/api/topics.py`, `backend/api/settings.py`, `backend/services/classifier.py` +- **Note:** This is a pre-existing issue in the codebase (also noted in 02-01 summary) that was blocking all tests + +**3. [Rule 1 - Bug] Slowapi IP-level counter bleed between tests** +- **Found during:** Task 1 GREEN phase (test suite) +- **Issue:** 10 tests calling `/api/auth/register` or `/api/auth/login` accumulated against IP `127.0.0.1`, causing `test_per_account_rate_limit` to fail when run in sequence (IP limit hit before per-account counter) +- **Fix:** Added slowapi storage reset (`auth_limiter._storage.reset()`) to `authed_client` fixture before each test +- **Files modified:** `backend/tests/test_auth_api.py` + +**4. [Rule 2 - Missing] email-validator package not installed locally** +- **Found during:** Task 1 GREEN phase (import test) +- **Issue:** Pydantic `EmailStr` requires `email-validator` package; not installed in local Python 3.9 env +- **Fix:** `pip install 'pydantic[email]'` locally (already in requirements for Docker env) +- **Note:** No file changes needed; package is implicitly included via `pydantic[email]` in Docker container + +## Known Stubs + +- `AdminView.vue`: tab navigation shell only — user/quota/AI config management deferred to Plan 02-03 (admin API endpoints) +- `AccountView.vue`: change-password and sign-out-all wired; TOTP enrollment UI deferred to Plan 02-04 +- `PasswordResetView.vue`, `NewPasswordView.vue`: forms wired to API but password-reset confirm endpoint not yet implemented in backend (deferred to Plan 02-05) +- None of these stubs prevent Plan 02's goal (auth wall live) from being achieved + +## Threat Flags + +None — all new endpoints follow STRIDE threat model mitigations: +- T-02-09: Identical 401 message for non-existent email and wrong password (anti-enumeration) +- T-02-11: SameSite=Strict + Origin middleware for CSRF prevention +- T-02-13: Dual rate limiting (IP-level slowapi + per-account Redis) +- T-02-14: CSP, X-Frame-Options, X-Content-Type-Options on all responses +- T-02-15: CORS wildcard removed +- T-02-16: password_must_change=True returns 200 with flag only, no tokens +- T-02-26: verify_backup_code marks used_at on first use; constant-time comparison +- T-02-27: per-account rate limit applies to all login paths including backup code + +## Self-Check + +See section below. + +--- + +## Self-Check: PASSED + +**Files verified:** + +- backend/api/auth.py — FOUND +- backend/main.py — contains `app.state.redis` (FOUND), `cors_origins` (FOUND), `Content-Security-Policy` (FOUND) +- backend/tests/test_auth_api.py — FOUND (17 tests, all passing) +- frontend/src/stores/auth.js — FOUND, no localStorage +- frontend/src/router/index.js — FOUND, contains beforeEach and /login +- frontend/src/layouts/AuthLayout.vue — FOUND +- frontend/src/views/auth/LoginView.vue — FOUND, contains "Sign in to DocuVault" and backupCode +- frontend/src/views/auth/RegisterView.vue — FOUND, contains "Create your account" +- frontend/src/components/auth/PasswordStrengthBar.vue — FOUND +- frontend/src/components/ui/AppSpinner.vue — FOUND, contains "animate-spin" +- frontend/src/api/client.js — FOUND, contains "Authorization", "Bearer", "_retry", "refresh", "changePassword" + +**Commits verified:** + +- 1d425d4 (test: RED phase — test_auth_api.py) — FOUND +- 1882edf (feat: GREEN phase — auth.py + main.py + Python 3.9 fixes) — FOUND +- 3b7d362 (feat: frontend auth store, router, views) — FOUND + +**Verification results:** +- pytest tests/test_auth_api.py: 17 passed +- npm run build: exits 0 +- grep -c allow_origins=["*"] backend/main.py: 0 +- grep -c localStorage frontend/src/stores/auth.js: 0 +- grep -c backup_code backend/api/auth.py: 7 + +--- +*Phase: 02-users-authentication* +*Completed: 2026-05-22*