---
phase: 08-stack-upgrade-backend-decomposition
plan: 06
type: execute
wave: 2
depends_on: [08-03]
files_modified:
  - backend/api/auth/__init__.py
  - backend/api/auth/tokens.py
  - backend/api/auth/totp.py
  - backend/api/auth/password.py
  - backend/api/auth/shared.py
  - backend/api/auth.py
autonomous: true
requirements: [CODE-03, CODE-08, CR-01, CR-02, CR-03]
tags: [backend-decomposition, auth, sub-router, session-revocation]
must_haves:
  truths:
    - "backend/api/auth/ is a Python package with sub-modules tokens.py, totp.py, password.py, shared.py and __init__.py"
    - "All auth endpoints respond on the same URL paths as before, including rate-limit decorators"
    - "limiter is defined in api/auth/shared.py and re-exported from api/auth/__init__.py so `from api.auth import limiter` continues to work in main.py and tests/conftest.py"
    - "change_password (in password.py), enable_totp (in totp.py), and disable_totp (in totp.py) preserve the existing revoke_all_refresh_tokens(skip_token_hash=...) call exactly — CR-01/CR-02/CR-03 tests promoted in plan 08-03 must still pass"
    - "No sub-router declares a prefix; only api/auth/__init__.py carries prefix=/api/auth"
    - "Old backend/api/auth.py monolith is deleted"
    - "Test file imports `from api.auth import limiter as auth_limiter` (conftest.py, test_auth_api.py, test_auth_totp.py, test_totp_replay.py, test_security_headers.py) continue to work without modification"
  artifacts:
    - path: "backend/api/auth/__init__.py"
      provides: "Router aggregator with prefix=/api/auth; includes tokens_router, totp_router, password_router; re-exports limiter"
      contains: "router = APIRouter(prefix"
    - path: "backend/api/auth/tokens.py"
      provides: "register, login, refresh, logout, logout-all, me, me/quota, me/preferences handlers"
      contains: "async def login"
    - path: "backend/api/auth/totp.py"
      provides: "totp/setup, totp/enable, totp DELETE (disable) handlers"
      contains: "async def enable_totp"
    - path: "backend/api/auth/password.py"
      provides: "change-password, password-reset, password-reset/confirm handlers"
      contains: "async def change_password"
    - path: "backend/api/auth/shared.py"
      provides: "Limiter instance, request/response Pydantic models, _set_refresh_cookie, _user_dict helpers"
      contains: "limiter = Limiter"
  key_links:
    - from: "backend/main.py"
      to: "backend/api/auth/__init__.py"
      via: "from api.auth import limiter as auth_limiter AND from api.auth import router as auth_router"
      pattern: "from api.auth import (limiter as auth_limiter|router as auth_router)"
    - from: "backend/tests/conftest.py and 4 other test files"
      to: "backend/api/auth/__init__.py"
      via: "from api.auth import limiter as auth_limiter"
      pattern: "from api.auth import limiter as auth_limiter"
    - from: "password.py change_password handler"
      to: "services.auth.revoke_all_refresh_tokens"
      via: "skip_token_hash=skip_hash argument"
      pattern: "revoke_all_refresh_tokens\\(.*skip_token_hash"
---

<objective>
Decompose the 825-line `backend/api/auth.py` monolith into the focused package `backend/api/auth/` per locked D-07 (4 sub-modules: tokens, totp, password, plus shared.py for cross-module helpers; module names mirror `services/auth.py` logical groupings). The Limiter instance must remain importable as `from api.auth import limiter` to avoid touching 5 test files plus `main.py`. The CR-01/CR-02/CR-03 session-revocation behavior (already implemented and now covered by passing tests from plan 08-03) MUST continue to work unchanged.

Purpose: CODE-03 (decomposition) + CODE-08 (single definition of auth Pydantic request models in `shared.py`) while preserving CR-01/CR-02/CR-03 production behavior and its passing test coverage.

Output: `backend/api/auth/` package; `backend/api/auth.py` monolith deleted; all auth + CR + rate-limit + security-header tests still pass.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md
@.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md
@.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md
@backend/api/auth.py
@backend/services/auth.py
@backend/main.py
@backend/tests/conftest.py
@CLAUDE.md

<interfaces>
Endpoint to sub-module map (locked per RESEARCH.md §"api/auth.py → api/auth/ package"):

tokens.py:   register (POST /register, line 110-188), login (POST /login, line 190-323),
             refresh_token (POST /refresh, line 325-387), logout (POST /logout, line 389-422),
             logout_all (POST /logout-all, line 424-449),
             get_me (GET /me, line 451-457), get_my_quota (GET /me/quota, line 459-475),
             get_my_preferences (GET /me/preferences, line 790-806),
             update_my_preferences (PATCH /me/preferences, line 808 onwards)
totp.py:     totp_setup (GET /totp/setup, line 557-577), enable_totp (POST /totp/enable, line 579-639),
             disable_totp (DELETE /totp, line 641-685)
password.py: change_password (POST /change-password, line 477-540),
             password_reset_request (POST /password-reset, line 687-720),
             password_reset_confirm (POST /password-reset/confirm, line 722-778)

Pydantic model + helper map (shared.py):

shared.py:   limiter (the slowapi Limiter instance, line 46),
             RegisterRequest (line 51-55), LoginRequest (line 57-63), ChangePasswordRequest (line 65-69),
             TotpEnableRequest (line 542-544), PasswordResetRequest (line 546-548),
             PasswordResetConfirmRequest (line 550-554), PreferencesUpdate (line 780-787),
             _set_refresh_cookie (line 72-94), _user_dict (line 96-105)

Package aggregator pattern for backend/api/auth/__init__.py — note the limiter re-export:

  from fastapi import APIRouter
  from api.auth.tokens import router as tokens_router
  from api.auth.totp import router as totp_router
  from api.auth.password import router as password_router
  from api.auth.shared import limiter  # re-exported so `from api.auth import limiter` still works

  router = APIRouter(prefix="/api/auth", tags=["auth"])
  router.include_router(tokens_router)
  router.include_router(totp_router)
  router.include_router(password_router)

Critical: 5 production/test files import `limiter`:
  - backend/main.py line 21
  - backend/tests/conftest.py line 222
  - backend/tests/test_auth_api.py line 99
  - backend/tests/test_auth_totp.py line 98
  - backend/tests/test_totp_replay.py line 76
  - backend/tests/test_security_headers.py line 72
None of these files may be modified by this plan — the `__init__.py` re-export keeps the import path stable.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create backend/api/auth/ package — shared.py, tokens.py, totp.py, password.py, __init__.py</name>
  <files>backend/api/auth/__init__.py, backend/api/auth/shared.py, backend/api/auth/tokens.py, backend/api/auth/totp.py, backend/api/auth/password.py</files>
  <read_first>
    - backend/api/auth.py lines 1-825 (full source — every endpoint, every Pydantic model, every helper, every import including hashlib for skip_hash computation, time for Redis nbf updates)
    - backend/services/auth.py (confirm `revoke_all_refresh_tokens(session, user_id, skip_token_hash=None)` signature; copy the calling pattern verbatim)
    - backend/main.py lines 20-30 and 246-260 (confirm `from api.auth import limiter as auth_limiter` and `app.state.limiter = auth_limiter` wiring)
    - backend/tests/conftest.py line 220-225 (auth_limiter reset hook)
    - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md sections for `backend/api/auth/*` (exact import lists, rate-limit decorator pattern, ValueError-to-HTTPException bridge, session revocation pattern)
    - CLAUDE.md §"Login token hardening" (must preserve: ES256, 15-min access TTL, JTI claim handling, fgp validation, refresh token rotation behavior in tokens.py)
  </read_first>
  <action>
This task creates the package and ALL five files. Execute in this order so the package is in a valid state on disk after each sub-step:

(A) Create `backend/api/auth/shared.py` first because every other sub-module imports from it. Add `from __future__ import annotations`. Imports: `from typing import Optional`, `from fastapi import Response`, `from pydantic import BaseModel, EmailStr`, `from slowapi import Limiter`, `from config import settings`, `from deps.utils import get_client_ip`. Then declare `limiter = Limiter(key_func=get_client_ip)` (line 46 of monolith). Move these Pydantic models verbatim from monolith: `RegisterRequest` (51-55), `LoginRequest` (57-63 — keep `remember_me: bool = False` field), `ChangePasswordRequest` (65-69), `TotpEnableRequest` (542-544), `PasswordResetRequest` (546-548), `PasswordResetConfirmRequest` (550-554), `PreferencesUpdate` (780-787). Move helpers verbatim: `_set_refresh_cookie(response, raw_token, remember_me=False)` (72-94 — note the remember_me-aware max_age branching from Phase 7.3) and `_user_dict(user)` (96-105).

(B) Create `backend/api/auth/tokens.py`. Header: `from __future__ import annotations`. Imports per PATTERNS.md §"backend/api/auth/tokens.py" plus `from api.auth.shared import limiter, RegisterRequest, LoginRequest, PreferencesUpdate, _set_refresh_cookie, _user_dict`. `router = APIRouter()` — NO prefix (D-04). Move handlers verbatim INCLUDING the `@limiter.limit("10/minute")` decorators: `register` (110-188), `login` (190-323), `refresh_token` (325-387), `logout` (389-422), `logout_all` (424-449), `get_me` (451-457), `get_my_quota` (459-475), `get_my_preferences` (790-806), `update_my_preferences` (808 onwards). The ES256 + JTI + fgp + Redis user_nbf logic in `login`/`refresh_token` is preserved verbatim (CLAUDE.md non-negotiable).

(C) Create `backend/api/auth/totp.py`. Header + imports per PATTERNS.md §"backend/api/auth/totp.py" plus `from api.auth.shared import limiter, TotpEnableRequest, _set_refresh_cookie`. `router = APIRouter()` — NO prefix. Move handlers verbatim: `totp_setup` (557-577), `enable_totp` (579-639), `disable_totp` (641-685). The CR-02 (enable_totp) and CR-03 (disable_totp) skip-hash + revoke_all_refresh_tokens + sessions_revoked logic is preserved verbatim — these handlers are covered by passing tests from plan 08-03, which this plan MUST not break. Copy `hashlib` and `time` imports as needed at the top.

(D) Create `backend/api/auth/password.py`. Header + imports per PATTERNS.md §"backend/api/auth/password.py" plus `from api.auth.shared import limiter, ChangePasswordRequest, PasswordResetRequest, PasswordResetConfirmRequest`. `router = APIRouter()` — NO prefix. Move handlers verbatim: `change_password` (477-540), `password_reset_request` (687-720), `password_reset_confirm` (722-778). The CR-01 (change_password) skip-hash + revoke_all_refresh_tokens + sessions_revoked + Redis user_nbf logic is preserved verbatim from monolith lines 514-540.

(E) Create `backend/api/auth/__init__.py`. Contents:

  from fastapi import APIRouter
  from api.auth.tokens import router as tokens_router
  from api.auth.totp import router as totp_router
  from api.auth.password import router as password_router
  from api.auth.shared import limiter  # re-export

  router = APIRouter(prefix="/api/auth", tags=["auth"])
  router.include_router(tokens_router)
  router.include_router(totp_router)
  router.include_router(password_router)

The `from api.auth.shared import limiter` line makes `from api.auth import limiter` work for `main.py` and the 5 test files (because module attribute access resolves through `__init__.py`).

To avoid the package-vs-module name collision, immediately after creating the package directory, rename `backend/api/auth.py` to `backend/api/auth_OLD_REMOVE_IN_TASK_2.py`. Task 2 verifies tests pass and deletes the renamed file.
  </action>
  <verify>
    <automated>cd backend && python -c "from api.auth import router, limiter; print('auth routes:', len(router.routes)); print('limiter type:', type(limiter).__name__)"</automated>
  </verify>
  <acceptance_criteria>
    - Files exist: `backend/api/auth/__init__.py`, `backend/api/auth/shared.py`, `backend/api/auth/tokens.py`, `backend/api/auth/totp.py`, `backend/api/auth/password.py`
    - `grep -v '^#' backend/api/auth/__init__.py | grep -c 'router = APIRouter(prefix="/api/auth"'` returns 1
    - `grep -v '^#' backend/api/auth/tokens.py | grep -c 'router = APIRouter()'` returns 1 (NO prefix)
    - `grep -v '^#' backend/api/auth/totp.py | grep -c 'router = APIRouter()'` returns 1 (NO prefix)
    - `grep -v '^#' backend/api/auth/password.py | grep -c 'router = APIRouter()'` returns 1 (NO prefix)
    - `grep -c "limiter = Limiter" backend/api/auth/shared.py` returns 1
    - `grep -c "from api.auth.shared import limiter" backend/api/auth/__init__.py` returns 1 (re-export)
    - `grep -c "skip_token_hash=skip_hash" backend/api/auth/password.py` returns 1 (CR-01 preserved)
    - `grep -c "skip_token_hash=skip_hash" backend/api/auth/totp.py` returns 2 (CR-02 enable_totp + CR-03 disable_totp preserved)
    - `cd backend && python -c "from api.auth import limiter, router; assert callable(getattr(limiter, 'limit', None)); assert len(router.routes) >= 14"` exits 0 (9 tokens + 3 totp + 3 password = at minimum 14, likely 15)
  </acceptance_criteria>
  <done>Package exists with all 5 files; limiter re-exported from __init__.py; CR-01/02/03 skip_token_hash calls preserved; monolith renamed (not yet deleted).</done>
</task>

<task type="auto">
  <name>Task 2: Run URL + CR regression suite, then delete the old monolith</name>
  <files>backend/api/auth.py</files>
  <read_first>
    - backend/tests/test_auth.py (the three CR tests from plan 08-03)
    - backend/tests/test_auth_api.py (rate-limit tests)
    - backend/tests/test_auth_totp.py (TOTP integration tests)
    - backend/tests/test_totp_replay.py (replay-prevention tests)
    - backend/tests/test_security_headers.py (CSP/security headers tests)
    - backend/api/auth_OLD_REMOVE_IN_TASK_2.py (renamed monolith)
  </read_first>
  <action>
Run the affected test files in sequence to confirm decomposition preserves: (1) URL paths, (2) rate-limit behavior (limiter import path), (3) CR-01/CR-02/CR-03 session revocation, (4) TOTP replay prevention, (5) CSP headers.

Command: `cd backend && pytest tests/test_auth.py tests/test_auth_api.py tests/test_auth_totp.py tests/test_totp_replay.py tests/test_security_headers.py -x -v`.

If ANY test fails, diagnose, fix the sub-module, and re-run. Special attention: if a test fails with `ImportError: cannot import name 'limiter' from 'api.auth'`, the `__init__.py` re-export line is missing or misspelled. If a CR test fails, compare the exact `skip_hash` computation block in `password.py` / `totp.py` against the original monolith — every byte must match.

Only after all five test files pass: delete `backend/api/auth_OLD_REMOVE_IN_TASK_2.py`. Re-run `cd backend && pytest -v` to confirm no other tests regressed.
  </action>
  <verify>
    <automated>cd backend && pytest tests/test_auth.py tests/test_auth_api.py tests/test_auth_totp.py tests/test_totp_replay.py tests/test_security_headers.py -x -v 2>&1 | tail -20 && test ! -f backend/api/auth.py && test ! -f backend/api/auth_OLD_REMOVE_IN_TASK_2.py && echo "old monolith deleted"</automated>
  </verify>
  <acceptance_criteria>
    - `test ! -f backend/api/auth.py` exits 0
    - `test ! -f backend/api/auth_OLD_REMOVE_IN_TASK_2.py` exits 0
    - `test -d backend/api/auth` exits 0
    - `cd backend && pytest tests/test_auth.py -x` exits 0 (includes the three promoted CR tests)
    - `cd backend && pytest tests/test_auth_api.py -x` exits 0
    - `cd backend && pytest tests/test_auth_totp.py -x` exits 0
    - `cd backend && pytest tests/test_totp_replay.py -x` exits 0
    - `cd backend && pytest tests/test_security_headers.py -x` exits 0
    - `cd backend && pytest tests/test_auth.py::test_change_password_revokes_other_sessions tests/test_auth.py::test_enable_totp_revokes_other_sessions tests/test_auth.py::test_disable_totp_revokes_other_sessions -v` shows all three as PASSED
    - `cd backend && pytest -v` exits 0 with no new failures vs. baseline
    - `grep -rn "^class RegisterRequest" backend/api/auth/` returns exactly one match (shared.py)
    - `grep -rn "^class LoginRequest" backend/api/auth/` returns exactly one match (shared.py)
    - No file in `backend/tests/` was modified by this plan (test imports still use `from api.auth import limiter`)
  </acceptance_criteria>
  <done>Old auth.py deleted; full backend suite green; CR-01/02/03 tests still PASSED; rate-limit / TOTP / security headers tests all green; no test file touched.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Unauthenticated client → /api/auth/login, /api/auth/register, /api/auth/refresh | Rate-limited via `@limiter.limit("10/minute")`; limiter must be the SAME instance app.state.limiter references |
| Authenticated user → /api/auth/change-password, /api/auth/totp/enable, /api/auth/totp (DELETE) | CR-01/CR-02/CR-03 require `revoke_all_refresh_tokens` with `skip_token_hash` for the current session — preserved verbatim |
| Authenticated user → /api/auth/refresh | Refresh-token rotation + family-revocation on reuse — preserved verbatim from monolith |
| Authenticated user → /api/auth/logout-all | Revoke all sessions including current; sets Redis user_nbf — preserved verbatim |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-08-06-01 | Spoofing | limiter re-export | mitigate | `__init__.py` re-exports `limiter` from `shared.py` so the SAME Limiter instance is accessible via the legacy import path. Acceptance criterion greps for the re-export line. |
| T-08-06-02 | Repudiation | CR-01/CR-02/CR-03 audit log entries | mitigate | `write_audit_log(..., metadata_={"sessions_revoked": revoked}, ...)` calls in change_password / enable_totp / disable_totp copied verbatim; passing tests from plan 08-03 verify behavior end-to-end. |
| T-08-06-03 | Tampering | Refresh-token rotation logic | mitigate | `refresh_token` handler in tokens.py copied verbatim including JTI, fgp, family-revocation, Redis nbf. |
| T-08-06-04 | Information Disclosure | Sub-router prefix doubling | mitigate | All three sub-routers declare `router = APIRouter()` with NO prefix; only `__init__.py` carries `prefix="/api/auth"`. |
| T-08-06-05 | Denial of Service | Circular import via `__init__.py` | mitigate | `__init__.py` does ONLY router aggregation + limiter re-export; shared models in `shared.py`. |
| T-08-06-06 | Elevation of Privilege | Session revocation skip | mitigate | `skip_token_hash=skip_hash` argument preserved in all three CR handlers; grep acceptance criterion counts occurrences. |
| T-08-06-SC | Supply Chain | No new packages | accept | This plan installs zero new packages. |
</threat_model>

<verification>
- `cd backend && pytest tests/test_auth.py tests/test_auth_api.py tests/test_auth_totp.py tests/test_totp_replay.py tests/test_security_headers.py -x -v` — zero failures
- `cd backend && pytest -v` — full suite zero failures
- `cd backend && python -c "from main import app; auth_paths = sorted({r.path for r in app.routes if r.path.startswith('/api/auth')}); print('\\n'.join(auth_paths))"` lists at minimum: `/api/auth/register`, `/api/auth/login`, `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/logout-all`, `/api/auth/me`, `/api/auth/me/quota`, `/api/auth/me/preferences`, `/api/auth/totp/setup`, `/api/auth/totp/enable`, `/api/auth/totp`, `/api/auth/change-password`, `/api/auth/password-reset`, `/api/auth/password-reset/confirm`
- `cd backend && python -c "from main import app; from api.auth import limiter; assert app.state.limiter is limiter"` — confirms identity (same instance, not a copy)
</verification>

<success_criteria>
- `backend/api/auth/` package with 5 files
- `backend/api/auth.py` deleted
- All auth URL paths unchanged
- limiter still importable via `from api.auth import limiter` (5 test files + main.py unchanged)
- CR-01/CR-02/CR-03 tests still PASSED (not regressed by decomposition)
- All tests pass
</success_criteria>

<output>
Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-06-SUMMARY.md` when done. Include: (a) full list of auth paths emitted by `app.routes` before vs. after (must be identical), (b) test counts for each of the 5 covered files, (c) confirmation that `app.state.limiter is api.auth.limiter` (identity check), (d) confirmation that no file under `backend/tests/` was modified.
</output>
