3 plans (3 waves): Wave 0 xfail stubs, Wave 1 ES256 core + startup rotation, Wave 2 remember-me TTL split + LoginView checkbox. Verification passed (0 blockers, 1 minor doc warning fixed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07.3-security-es256-algorithm-upgrade-inserted | 01 | execute | 0 |
|
true |
|
|
Purpose: Enforce TDD discipline; guarantee every locked decision (D-01..D-12) has an automated check.
Output: One new test file (test_auth_es256.py) with 9 xfail stubs + a reusable ES256 key fixture, plus an extension to the existing config test asserting refresh_token_expire_hours == 16.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/STATE.md @.planning/ROADMAP.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-CONTEXT.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-RESEARCH.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-PATTERNS.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-VALIDATION.md @backend/tests/test_task1_models_config.py @backend/tests/test_task2_auth_service.py @backend/tests/test_auth_api.py @backend/tests/conftest.pyFrom backend/tests/conftest.py:
- pytest_asyncio.fixture
async_client— httpx.AsyncClient bound to the FastAPI app (used for integration tests against /api/auth/login) - pytest_asyncio.fixture
db_session— AsyncSession for direct row inspection (used to assert RefreshToken.expires_at after login) - pytest_asyncio.fixture
auth_user— returns dict withemail,password,idfor a real registered user
From backend/tests/test_auth_api.py (lines 47-97 area):
- class
FakeRedis— minimal in-memory stub exposingget(key),set(key, value, ex=...),delete(key)used to bypass real Redis during integration tests - helper
_register(client, email, password)— registers a user via POST /api/auth/register - helper
_login(client, email, password, **extra)— POSTs /api/auth/login and returns the response
From backend/services/auth.py:
def create_access_token(user_id: str, role: str) -> str # currently HS256 — Plan 02 swaps to ES256
def create_password_reset_token(user_id: str) -> str # currently HS256 — Plan 02 swaps to ES256
def decode_access_token(token: str) -> dict # currently HS256 — Plan 02 swaps to ES256
async def create_refresh_token(session, user_id) -> str # Plan 03 adds remember_me param
From backend/config.py (current JWT block, lines 33-35):
access_token_expire_minutes: int = 15
refresh_token_expire_days: int = 30
# Plan 02 adds: refresh_token_expire_hours: int = 16, jwt_private_key: str = "", jwt_public_key: str = ""
From cryptography library (already in requirements.txt):
ec.generate_private_key(ec.SECP256R1())— generate P-256 private keyserialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.PublicFormat.SubjectPublicKeyInfo— PEM serialization formats
Define an `autouse=True` module-level fixture `es256_keys(monkeypatch)` that (a) generates a P-256 private key with `ec.generate_private_key(ec.SECP256R1())` from cryptography.hazmat.primitives.asymmetric, (b) serializes private key as PEM PKCS8 NoEncryption then base64.b64encode().decode(), (c) serializes public key as PEM SubjectPublicKeyInfo then base64.b64encode().decode(), (d) monkeypatches `config.settings.jwt_private_key` and `config.settings.jwt_public_key` to these base64 strings. This fixture exists in Wave 0 even though those settings fields do not exist yet — `monkeypatch.setattr` with `raising=False` so it does not error before Plan 02 lands.
Declare exactly these 9 stubs, each guarded with `@pytest.mark.xfail(strict=True, reason="<REQ-ID>: not yet implemented")`. Function bodies contain ONLY `pytest.xfail("not yet implemented")` (single-line body) — no test logic. The reason strings and function names MUST match the VALIDATION.md per-task table exactly:
1. `test_access_token_uses_es256()` — ES256-01 — sync, no fixtures beyond es256_keys
2. `test_hs256_token_rejected()` — ES256-02 — sync; will craft HS256 token with PyJWT and pass to decode_access_token
3. `test_reset_token_uses_es256()` — ES256-03 — sync
4. `test_startup_rotation_revokes_tokens(db_session)` — ES256-04 — `@pytest.mark.asyncio async def`; uses db_session
5. `test_startup_rotation_idempotent(db_session)` — ES256-05 — `@pytest.mark.asyncio async def`
6. `test_default_ttl_16_hours(async_client, db_session, auth_user)` — RM-01 — `@pytest.mark.asyncio async def`
7. `test_remember_me_ttl_30_days(async_client, db_session, auth_user)` — RM-02 — `@pytest.mark.asyncio async def`
8. `test_remember_me_cookie_max_age(async_client, auth_user)` — RM-03 — `@pytest.mark.asyncio async def`
9. `test_settings_has_jwt_keys()` — CFG-01 satellite for jwt_private_key/jwt_public_key presence in settings — sync
Each stub MUST be reachable by `pytest --collect-only`. Use `strict=True` on xfail so that any premature pass breaks CI and forces explicit promotion. Do NOT import services.auth or main at module top level (it would crash before Plan 02 adds settings fields) — perform imports lazily inside test bodies where needed (already redundant because bodies only call `pytest.xfail`).
Do not write any assertion code. Do not implement the test bodies — Plans 02 and 03 promote each stub by replacing the `xfail` body with real assertions in the same task that lands the corresponding production change.
Add the following new assertions inside the same test function (after the existing ones), each on its own line:
- `assert hasattr(settings, "refresh_token_expire_hours")` — CFG-01
- `assert settings.refresh_token_expire_hours == 16` — CFG-01 / D-09
- `assert hasattr(settings, "jwt_private_key")` — CFG-01 / D-01
- `assert hasattr(settings, "jwt_public_key")` — CFG-01 / D-01
These assertions will FAIL until Plan 02 adds the three new fields to backend/config.py — that is intentional (Nyquist). Do NOT wrap this test in xfail. The test must hard-fail today and hard-pass after Plan 02. Plan 02 promotes by simply adding the fields; the test then passes automatically without code changes here.
Do not change any other tests in this file. Do not add new imports beyond what is already present (`from config import settings`).
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| pytest runner → backend modules | Test code imports production modules; lazy imports prevent load-time crashes before Plan 02 |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-07.3-01-01 | Tampering | Wave 0 stubs flipping to xpass silently | mitigate | strict=True on every @pytest.mark.xfail forces CI failure on unexpected pass; promotion is explicit (Plan 02 / Plan 03 must edit the file) |
| T-07.3-01-02 | Spoofing | Test fixture leaking real JWT keys into other tests | mitigate | es256_keys fixture uses freshly-generated P-256 keypair per module via monkeypatch — auto-reverted after each test; never reads real env vars |
| T-07.3-01-03 | Denial of Service | Module-level import of services.auth crashing before Plan 02 | mitigate | All imports of services.auth and config.settings.jwt_* are lazy (inside test bodies); module collects cleanly even before Plan 02 |
| T-07.3-01-SC | Tampering | npm/pip/cargo installs | accept | No new packages introduced; PyJWT and cryptography already pinned in requirements.txt (per RESEARCH.md §Package Legitimacy Audit — slopcheck not required) |
| </threat_model> |
<success_criteria>
- backend/tests/test_auth_es256.py exists with 9 xfail stubs, all strict=True
- es256_keys autouse fixture generates a fresh P-256 keypair and monkeypatches settings.jwt_private_key / jwt_public_key (uses raising=False)
- backend/tests/test_task1_models_config.py::test_settings_has_jwt_config asserts refresh_token_expire_hours == 16 and presence of jwt_private_key / jwt_public_key
- No production source files modified in this plan
- pytest collects all stubs cleanly; collection does not require Plan 02 fields to exist </success_criteria>