--- 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*