Files
kite/.planning/milestones/v0.2-phases/08-stack-upgrade-backend-decomposition/08-06-PLAN.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to
.planning/milestones/v0.2-phases/ to keep .planning/phases/ clean
for the next milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:34:52 +02:00

20 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
phase plan type wave depends_on files_modified autonomous requirements tags must_haves
08-stack-upgrade-backend-decomposition 06 execute 2
08-03
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
true
CODE-03
CODE-08
CR-01
CR-02
CR-03
backend-decomposition
auth
sub-router
session-revocation
truths artifacts key_links
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
path provides contains
backend/api/auth/__init__.py Router aggregator with prefix=/api/auth; includes tokens_router, totp_router, password_router; re-exports limiter router = APIRouter(prefix
path provides contains
backend/api/auth/tokens.py register, login, refresh, logout, logout-all, me, me/quota, me/preferences handlers async def login
path provides contains
backend/api/auth/totp.py totp/setup, totp/enable, totp DELETE (disable) handlers async def enable_totp
path provides contains
backend/api/auth/password.py change-password, password-reset, password-reset/confirm handlers async def change_password
path provides contains
backend/api/auth/shared.py Limiter instance, request/response Pydantic models, _set_refresh_cookie, _user_dict helpers limiter = Limiter
from to via pattern
backend/main.py backend/api/auth/__init__.py from api.auth import limiter as auth_limiter AND from api.auth import router as auth_router from api.auth import (limiter as auth_limiter|router as auth_router)
from to via pattern
backend/tests/conftest.py and 4 other test files backend/api/auth/__init__.py from api.auth import limiter as auth_limiter from api.auth import limiter as auth_limiter
from to via pattern
password.py change_password handler services.auth.revoke_all_refresh_tokens skip_token_hash=skip_hash argument revoke_all_refresh_tokens(.*skip_token_hash
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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 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.
Task 1: Create backend/api/auth/ package — shared.py, tokens.py, totp.py, password.py, __init__.py 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 - 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) 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. cd backend && python -c "from api.auth import router, limiter; print('auth routes:', len(router.routes)); print('limiter type:', type(limiter).name)" <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> 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).

Task 2: Run URL + CR regression suite, then delete the old monolith backend/api/auth.py - 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) 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. 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" <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> 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.

<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>
- `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)

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