---
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=<original-path>`
- 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*
