227 lines
14 KiB
Markdown
227 lines
14 KiB
Markdown
---
|
|
phase: 07.1
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on:
|
|
- 07.1-01
|
|
files_modified:
|
|
- backend/tests/test_auth_api.py
|
|
- frontend/src/components/settings/SettingsAccountTab.vue
|
|
- frontend/src/components/auth/TotpEnrollment.vue
|
|
autonomous: true
|
|
requirements:
|
|
- CR-01
|
|
- CR-02
|
|
- CR-03
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Tests verify sessions_revoked > 0 when other sessions exist for change_password"
|
|
- "Tests verify sessions_revoked > 0 when other sessions exist for enable_totp"
|
|
- "Tests verify sessions_revoked > 0 when other sessions exist for disable_totp"
|
|
- "Tests verify current session is NOT revoked (skip behavior)"
|
|
- "Frontend shows a toast notification when sessions_revoked > 0 after password change"
|
|
- "Frontend shows a toast notification when sessions_revoked > 0 after TOTP enable"
|
|
- "Frontend shows a toast notification when sessions_revoked > 0 after TOTP disable"
|
|
artifacts:
|
|
- path: "backend/tests/test_auth_api.py"
|
|
provides: "3 new tests covering sessions_revoked behavior + skip for all three endpoints"
|
|
contains: "sessions_revoked"
|
|
- path: "frontend/src/components/settings/SettingsAccountTab.vue"
|
|
provides: "toast notification on sessions_revoked > 0 for changePassword and disableTotp"
|
|
contains: "sessions_revoked"
|
|
- path: "frontend/src/components/auth/TotpEnrollment.vue"
|
|
provides: "emit or callback for sessions_revoked > 0 after enable_totp"
|
|
contains: "sessions_revoked"
|
|
key_links:
|
|
- from: "SettingsAccountTab.vue (changePassword)"
|
|
to: "SettingsView.vue toast pattern"
|
|
via: "local sessionRevokedToast ref, same inline HTML pattern as oauthSuccessProvider toast"
|
|
pattern: "sessions_revoked"
|
|
- from: "TotpEnrollment.vue (confirmEnrollment)"
|
|
to: "SettingsAccountTab.vue parent"
|
|
via: "emit('enrolled', { sessions_revoked }) OR local toast inside TotpEnrollment"
|
|
pattern: "sessions_revoked"
|
|
---
|
|
|
|
<objective>
|
|
Add tests proving the skip-current-session behavior for all three privilege-change endpoints, and add a brief frontend toast notification triggered when `sessions_revoked > 0` is returned from `change_password`, `enable_totp`, or `disable_totp`.
|
|
|
|
Purpose: Closes the test coverage gap (CLAUDE.md testing protocol: every new behavior must have at least one test) and delivers the user-facing UX signal described in D-05.
|
|
Output: 3 new pytest tests in `test_auth_api.py`; toast in `SettingsAccountTab.vue` and `TotpEnrollment.vue`.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/07.1-security-session-revocation-on-privilege-change/07.1-CONTEXT.md
|
|
@.planning/phases/07.1-security-session-revocation-on-privilege-change/07.1-01-SUMMARY.md
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Add tests for sessions_revoked behavior on all three endpoints</name>
|
|
<files>backend/tests/test_auth_api.py</files>
|
|
|
|
<read_first>
|
|
backend/tests/test_auth_api.py — read lines 1-50 (imports, fixture helpers `_register` and `_login`) and lines 240-258 (`test_change_password_success` — the pattern to extend). These show how to register a user, obtain a token, and call change-password.
|
|
backend/tests/conftest.py — check the `authed_client`, `auth_user`, and `db_session` fixture definitions to understand what is available.
|
|
backend/api/auth.py — check the updated `change_password`, `enable_totp`, and `disable_totp` response shapes from Plan 01 (to match `sessions_revoked` key name exactly).
|
|
</read_first>
|
|
|
|
<behavior>
|
|
- test_change_password_revokes_other_sessions: Register user, log in on two different clients (simulate two sessions by inserting a second RefreshToken row directly in the DB or calling create_refresh_token twice), then call POST /api/auth/change-password with the first session's token. Assert response contains `sessions_revoked >= 1`. Assert the second refresh token row is now `revoked=True` in the DB. Assert the first (calling) session's refresh token row is still `revoked=False` (current session was skipped). Note: `authed_client` does not set a refresh_token cookie by default — insert a second RefreshToken row via `auth_service.create_refresh_token(db_session, user.id)` to create a revocable "other session", then assert its row is revoked after the call.
|
|
- test_enable_totp_revokes_other_sessions: Register user, set `user.totp_secret` via DB fixture (same pattern as existing `test_login_backup_code_success` at line ~308), insert a second RefreshToken row, call POST /api/auth/totp/enable with a patched `verify_totp` returning True. Assert `sessions_revoked >= 1` in response and the second token row is revoked.
|
|
- test_disable_totp_revokes_other_sessions: Register user, set `user.totp_enabled=True` in DB, insert a second RefreshToken row, call DELETE /api/auth/totp. Assert `sessions_revoked >= 1` in response and the second token row is revoked.
|
|
</behavior>
|
|
|
|
<action>
|
|
Append three new `@pytest.mark.asyncio` test functions to `test_auth_api.py` after the existing `test_change_password_success` test.
|
|
|
|
For each test, import pattern: use `from services import auth as auth_service` (already available in the test module via existing imports — verify before adding) and the `db_session: AsyncSession` fixture.
|
|
|
|
To simulate "other sessions": call `await auth_service.create_refresh_token(db_session, user.id)` to insert an additional token row. This is the canonical way to create a second session without running a full login flow (which requires the refresh cookie roundtrip).
|
|
|
|
After the privilege-change API call, query `select(RefreshToken).where(RefreshToken.user_id == user.id)` and inspect the rows:
|
|
- The row matching the token created by `create_refresh_token` (the "other session") must have `revoked=True`.
|
|
- If the `authed_client` fixture set a real refresh_token cookie, the calling session's row must have `revoked=False`. If the fixture does NOT set a cookie (verify by reading conftest), then `skip_token_hash` will be `None` and all tokens including the "other" one will be revoked — the test still asserts `sessions_revoked >= 1`.
|
|
|
|
For `test_enable_totp_revokes_other_sessions`: patch `services.auth.verify_totp` to return `True` so the TOTP code check is bypassed (same pattern as existing TOTP tests). Also patch `services.auth.store_backup_codes` to avoid real Argon2 hashing overhead.
|
|
|
|
Import `from sqlalchemy import select` and `from models import RefreshToken` for the DB assertion queries — check existing imports in the test file first to avoid duplicates.
|
|
|
|
Use `with patch(...)` context managers exactly as in the existing breach-check tests.
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_auth_api.py -k "revokes_other_sessions" -v 2>&1 | tail -20</automated>
|
|
</verify>
|
|
|
|
<acceptance_criteria>
|
|
- `pytest tests/test_auth_api.py -k "revokes_other_sessions" -v` reports 3 PASSED tests with no failures
|
|
- Each test asserts `resp.json()["sessions_revoked"] >= 1`
|
|
- Each test queries the DB and asserts the "other session" RefreshToken row has `revoked=True`
|
|
- Full test suite `pytest -x -q` still passes with no regressions
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add sessions-revoked toast to SettingsAccountTab and TotpEnrollment</name>
|
|
<files>
|
|
frontend/src/components/settings/SettingsAccountTab.vue
|
|
frontend/src/components/auth/TotpEnrollment.vue
|
|
</files>
|
|
|
|
<read_first>
|
|
frontend/src/components/settings/SettingsAccountTab.vue — read the full file (254 lines). Observe: `changePassword()` at line 193 calls `api.changePassword(...)` and sets `passwordSuccess`; `disableTotp()` at line 226 calls `api.totpDisable()`. Note there is no existing toast in this component — the toast pattern to follow is in `SettingsView.vue` (the `oauthSuccessProvider` inline HTML block at lines 6-28).
|
|
frontend/src/views/SettingsView.vue — read lines 1-30 (the OAuth success toast block). This is the exact inline HTML pattern to replicate in SettingsAccountTab.
|
|
frontend/src/components/auth/TotpEnrollment.vue — read lines 145-166 (`confirmEnrollment()` function that calls `api.totpEnable()`). Note the `emit('enrolled')` call at line 165 and the `backupCodes` data returned at line 150.
|
|
</read_first>
|
|
|
|
<action>
|
|
### SettingsAccountTab.vue changes
|
|
|
|
Add a new reactive ref `sessionRevokedToast = ref(false)` alongside the existing refs in the `<script setup>` block.
|
|
|
|
Add a toast HTML block in the `<template>`, positioned ABOVE the `<div class="space-y-6">` container, following the exact same structural pattern as the OAuth success toast in SettingsView.vue:
|
|
- `v-if="sessionRevokedToast"` condition
|
|
- `class="fixed top-4 right-4 z-50 ..."` positioning (same Tailwind classes)
|
|
- Green success icon (same SVG as the OAuth toast)
|
|
- Message text: "Other sessions have been terminated." (per D-05)
|
|
- Dismiss button that sets `sessionRevokedToast = false`
|
|
|
|
In the `changePassword()` function: capture the response from `api.changePassword(...)` as `const data = await api.changePassword(...)`. After the existing `passwordSuccess.value = 'Password updated.'` line, add:
|
|
```
|
|
if (data.sessions_revoked > 0) {
|
|
sessionRevokedToast.value = true
|
|
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
|
|
}
|
|
```
|
|
|
|
In the `disableTotp()` function: capture the response as `const data = await api.totpDisable()`. After the existing `confirmDisable2fa.value = false` line, add:
|
|
```
|
|
if (data.sessions_revoked > 0) {
|
|
sessionRevokedToast.value = true
|
|
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
|
|
}
|
|
```
|
|
|
|
### TotpEnrollment.vue changes
|
|
|
|
In `confirmEnrollment()` at the `api.totpEnable()` call: capture the response as `const data = await api.totpEnable(verifyCode.value)`. The existing code already uses `data.backup_codes` at line 150 — keep that. After the `backupCodes.value = data.backup_codes` line, add:
|
|
```
|
|
if (data.sessions_revoked > 0) {
|
|
sessionRevokedToast.value = true
|
|
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
|
|
}
|
|
```
|
|
|
|
Add `const sessionRevokedToast = ref(false)` to the reactive state in `<script setup>`.
|
|
|
|
Add the same toast HTML block to TotpEnrollment's template, scoped to the component's root element (not fixed-position, since this is a component not a full view — use relative positioning: `class="mb-4 flex items-center gap-3 bg-white border border-green-200 rounded-xl px-5 py-4"` inside the component's template root). Show it `v-if="sessionRevokedToast"` with the text "Other sessions have been terminated." and a dismiss button.
|
|
|
|
Do NOT use a fixed-position toast inside TotpEnrollment (it is an embedded component, not a top-level view). Use inline alert style instead.
|
|
|
|
No new dependencies. No store changes. No route changes.
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -10</automated>
|
|
</verify>
|
|
|
|
<acceptance_criteria>
|
|
- `grep -n "sessions_revoked" frontend/src/components/settings/SettingsAccountTab.vue` returns at least 2 lines (changePassword + disableTotp handlers)
|
|
- `grep -n "sessions_revoked" frontend/src/components/auth/TotpEnrollment.vue` returns at least 1 line (confirmEnrollment handler)
|
|
- `grep -n "sessionRevokedToast" frontend/src/components/settings/SettingsAccountTab.vue` returns at least 3 lines (ref declaration + v-if + setTimeout)
|
|
- `grep -n "sessionRevokedToast" frontend/src/components/auth/TotpEnrollment.vue` returns at least 3 lines (ref declaration + v-if + setTimeout)
|
|
- `npm run build` in the frontend directory exits with code 0
|
|
- "Other sessions have been terminated." text present in both files
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| API response → frontend | `sessions_revoked` int from API response drives toast display — no user-supplied data rendered as HTML |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-7.1-04 | Information Disclosure | sessions_revoked toast | accept | Toast shows count-based boolean (>0), not the actual count or token details — no PII exposed |
|
|
| T-7.1-05 | Tampering | test fixture DB inserts | accept | Tests use internal `auth_service.create_refresh_token` — no external input path |
|
|
| T-7.1-SC | Tampering | npm/pip installs | accept | No new packages installed in this plan — no legitimacy gate required |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
After both tasks complete:
|
|
|
|
1. `cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_auth_api.py -k "revokes_other_sessions" -v` — 3 PASSED
|
|
2. `cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest -x -q` — zero failures (existing test count unchanged + 3 new passing)
|
|
3. `cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build` — exits 0
|
|
4. `grep -c "sessions_revoked" frontend/src/components/settings/SettingsAccountTab.vue` — 2 or more
|
|
5. `grep -c "sessions_revoked" frontend/src/components/auth/TotpEnrollment.vue` — 1 or more
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 3 new tests in `test_auth_api.py` named `test_*_revokes_other_sessions`, all PASSING
|
|
- Each test asserts `resp.json()["sessions_revoked"] >= 1`
|
|
- Frontend `SettingsAccountTab.vue` shows a 5-second "Other sessions have been terminated." toast after `changePassword` and `disableTotp` when `sessions_revoked > 0`
|
|
- Frontend `TotpEnrollment.vue` shows an inline "Other sessions have been terminated." alert after `enable_totp` when `sessions_revoked > 0`
|
|
- Full `pytest -x -q` passes with zero failures
|
|
- `npm run build` exits 0
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/07.1-security-session-revocation-on-privilege-change/07.1-02-SUMMARY.md` when done.
|
|
</output>
|