docs(07.1): create phase plan — 2 waves, CR-01..03 session revocation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-05 12:27:24 +02:00
co-authored by Claude Sonnet 4.6
parent 0fa23f5211
commit a3a97430a2
3 changed files with 424 additions and 2 deletions
+10 -2
View File
@@ -451,7 +451,15 @@ Before any phase is marked complete, all three gates must pass:
**Depends on**: Phase 7 **Depends on**: Phase 7
**Requirements**: CR-01, CR-02, CR-03 **Requirements**: CR-01, CR-02, CR-03
**Status:** Context gathered — ready for planning **Plans**: 2 plans
**Wave 1** — Backend: service + API changes
- [ ] 07.1-01-PLAN.md — Add skip_token_hash param to revoke_all_refresh_tokens + wire revoke into change_password, enable_totp, disable_totp with sessions_revoked response + audit log
**Wave 2** *(blocked on Wave 1)*
- [ ] 07.1-02-PLAN.md — Tests (3 new test_*_revokes_other_sessions) + frontend toast in SettingsAccountTab.vue + TotpEnrollment.vue
--- ---
@@ -501,7 +509,7 @@ Before any phase is marked complete, all three gates must pass:
| 6.1. Close v1.0 audit gaps | 2/2 | Complete | 2026-05-30 | | 6.1. Close v1.0 audit gaps | 2/2 | Complete | 2026-05-30 |
| 6.2. Close v1 sharing + cloud-delete + CSV export gaps | 5/5 | Complete | 2026-05-31 | | 6.2. Close v1 sharing + cloud-delete + CSV export gaps | 5/5 | Complete | 2026-05-31 |
| 7. Redo and optimize LLM integration | 5/5 | Complete | 2026-06-05 | | 7. Redo and optimize LLM integration | 5/5 | Complete | 2026-06-05 |
| 7.1. Security: session revocation on privilege change (CR-01..03) | 0/? | Context gathered | — | | 7.1. Security: session revocation on privilege change (CR-01..03) | 0/2 | Planned | — |
| 7.2. Security: JTI claim + Redis access-token revocation | 0/? | Not planned (INSERTED) | — | | 7.2. Security: JTI claim + Redis access-token revocation | 0/? | Not planned (INSERTED) | — |
| 7.3. Security: ES256 algorithm upgrade | 0/? | Not planned (INSERTED) | — | | 7.3. Security: ES256 algorithm upgrade | 0/? | Not planned (INSERTED) | — |
| 7.4. Security: token fingerprinting / token binding | 0/? | Not planned (INSERTED) | — | | 7.4. Security: token fingerprinting / token binding | 0/? | Not planned (INSERTED) | — |
@@ -0,0 +1,188 @@
---
phase: 07.1
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/services/auth.py
- backend/api/auth.py
autonomous: true
requirements:
- CR-01
- CR-02
- CR-03
must_haves:
truths:
- "Changing password revokes all other refresh tokens; current session stays alive"
- "Enabling TOTP revokes all other refresh tokens; current session stays alive"
- "Disabling TOTP revokes all other refresh tokens; current session stays alive"
- "All three endpoints return sessions_revoked: int in their response body"
- "The revocation count is written to the audit log metadata_ for all three operations"
artifacts:
- path: "backend/services/auth.py"
provides: "revoke_all_refresh_tokens with skip_token_hash optional param"
contains: "skip_token_hash: Optional[str] = None"
- path: "backend/api/auth.py"
provides: "revoke call + sessions_revoked in change_password, enable_totp, disable_totp"
contains: "sessions_revoked"
key_links:
- from: "backend/api/auth.py (change_password)"
to: "backend/services/auth.py (revoke_all_refresh_tokens)"
via: "await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=...)"
pattern: "skip_token_hash"
- from: "backend/api/auth.py (enable_totp)"
to: "backend/services/auth.py (revoke_all_refresh_tokens)"
via: "await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=...)"
pattern: "skip_token_hash"
- from: "backend/api/auth.py (disable_totp)"
to: "backend/services/auth.py (revoke_all_refresh_tokens)"
via: "await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=...)"
pattern: "skip_token_hash"
---
<objective>
Implement the three missing session-revocation calls (CR-01, CR-02, CR-03) by:
1. Extending `revoke_all_refresh_tokens` in `services/auth.py` with an optional `skip_token_hash` parameter so callers can exclude the current session.
2. Wiring the revoke call into the `change_password`, `enable_totp`, and `disable_totp` handlers in `api/auth.py`, deriving `skip_token_hash` from the request's refresh token cookie, writing the count to the audit log, and returning `sessions_revoked` in each response.
Purpose: Enforces the CLAUDE.md invariant — "Password change, TOTP enroll/revoke, and account deactivation immediately revoke all active sessions."
Output: Modified `services/auth.py` and `api/auth.py`; no migrations, no new routes.
</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/phases/07.1-security-session-revocation-on-privilege-change/07.1-CONTEXT.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Add skip_token_hash param to revoke_all_refresh_tokens</name>
<files>backend/services/auth.py</files>
<read_first>
backend/services/auth.py — read the full function at lines 218-237 (revoke_all_refresh_tokens), lines 154-172 (create_refresh_token, shows SHA-256 hash pattern), and lines 176-215 (rotate_refresh_token, shows how raw cookie value is hashed for DB lookup — identical pattern needed here).
</read_first>
<action>
Modify `revoke_all_refresh_tokens(session: AsyncSession, user_id: uuid.UUID)` to accept an additional optional parameter `skip_token_hash: Optional[str] = None`.
Update the WHERE clause in the SQLAlchemy `select(RefreshToken)` query to also filter out the token to skip when `skip_token_hash` is not None. The filter must add `RefreshToken.token_hash != skip_token_hash` as an additional condition in the `where()` call — only when the param is not None. When `skip_token_hash is None` (the existing `logout_all` caller), behavior is completely unchanged: all revoked=False tokens for user_id are revoked.
The `Optional` import is already present in the file (verify before adding). Do not change the function signature for any other caller — the default `None` value ensures backwards compatibility.
Do NOT change the row-by-row loop revocation logic. Bulk UPDATE optimization is explicitly out of scope per CONTEXT.md.
</action>
<verify>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/backend && grep -n "skip_token_hash" services/auth.py</automated>
</verify>
<acceptance_criteria>
- `services/auth.py` contains `async def revoke_all_refresh_tokens(session: AsyncSession, user_id: uuid.UUID, skip_token_hash: Optional[str] = None) -> int:`
- The WHERE clause excludes `skip_token_hash` when it is not None: `RefreshToken.token_hash != skip_token_hash` appears in the updated query body
- The existing `logout_all` caller in `api/auth.py` (line ~410) continues to call `revoke_all_refresh_tokens(session, current_user.id)` with no third argument — no change required there
- `grep -n "skip_token_hash" backend/services/auth.py` returns at least 2 lines (signature + WHERE usage)
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Wire revoke into change_password, enable_totp, disable_totp</name>
<files>backend/api/auth.py</files>
<read_first>
backend/api/auth.py — read the following sections:
- Lines 399-423 (logout_all handler) — this is the canonical pattern: revoke + write_audit_log metadata + commit + response
- Lines 454-503 (change_password handler) — add revoke before the existing session.commit()
- Lines 545-590 (enable_totp handler) — add revoke before the existing session.commit()
- Lines 595-624 (disable_totp handler) — add revoke before the existing session.commit()
Also verify the import block at the top of auth.py contains `import hashlib` (needed for SHA-256 hashing).
</read_first>
<action>
Apply the same pattern to all three handlers. The pattern is identical for each:
1. Derive the skip hash from the request cookie before the revoke call:
Read `raw_cookie = request.cookies.get("refresh_token")` then compute
`skip_hash = hashlib.sha256(raw_cookie.encode()).hexdigest() if raw_cookie else None`
2. Call revoke with the skip hash after the existing audit_log call but before `session.commit()`:
`revoked = await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)`
3. Add `sessions_revoked` to the audit log metadata_ in the existing `write_audit_log` call for each handler — extend the existing `metadata_={}` dict, or add `metadata_={"sessions_revoked": revoked}` if the current call has no metadata_ argument. Per D-06, this mirrors the `logout_all` pattern at line 419.
4. Return `sessions_revoked` in the response dict. Exact shapes:
- `change_password` currently returns `{"message": "Password updated"}` — change to `{"message": "Password updated", "sessions_revoked": revoked}`
- `enable_totp` currently returns `{"backup_codes": plain_codes}` — change to `{"backup_codes": plain_codes, "sessions_revoked": revoked}`
- `disable_totp` currently returns `{"message": "TOTP disabled"}` — change to `{"message": "TOTP disabled", "sessions_revoked": revoked}`
Per D-02/D-03: `request` is already a parameter on all three handlers (check the handler signatures; `change_password` and `disable_totp` already have `request: Request`; `enable_totp` already has `request: Request` for rate limiting). No signature changes needed.
Ensure `import hashlib` is present in the import section — it is already used in `services/auth.py:161`; verify it is also imported in `api/auth.py` before adding the hash computation.
Placement rule: `revoke_all_refresh_tokens` call goes BEFORE `session.commit()` in each handler (so it participates in the same transaction flush). The existing `write_audit_log` call uses `session.flush()` internally, so the order is: derive skip_hash → revoke → extend audit metadata → commit → return response.
Do NOT modify the `logout_all` handler — it already correctly revokes without skip (all sessions, including current).
</action>
<verify>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/backend && grep -n "sessions_revoked" api/auth.py</automated>
</verify>
<acceptance_criteria>
- `grep -n "sessions_revoked" backend/api/auth.py` returns at least 6 lines (3 revoke calls + 3 response dicts)
- `grep -n "skip_token_hash" backend/api/auth.py` returns at least 3 lines (one per handler)
- `grep -n "hashlib.sha256" backend/api/auth.py` returns at least 3 lines (one per handler deriving the skip hash)
- The `logout_all` handler is unchanged: `grep -n "logout_all" backend/api/auth.py` shows the handler still calls `revoke_all_refresh_tokens(session, current_user.id)` with no `skip_token_hash` argument
- `cd backend && python -c "import api.auth"` exits with code 0 (no import errors)
</acceptance_criteria>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| client cookie → API | Raw refresh token arrives in httpOnly cookie; must not be logged or echoed |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-7.1-01 | Tampering | skip_token_hash derivation | mitigate | Hash computed server-side with hashlib.sha256 — cookie value never compared in plaintext; constant-time exclusion via WHERE clause (not Python ==) |
| T-7.1-02 | Information Disclosure | sessions_revoked response field | accept | Count of revoked sessions is low-sensitivity metadata; exact token values never exposed |
| T-7.1-03 | Elevation of Privilege | missing skip on logout_all | accept | logout_all intentionally revokes ALL sessions (no skip) — behaviour unchanged, tested separately |
| 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 -c "import api.auth; import services.auth"` — exits 0
2. `grep -c "sessions_revoked" backend/api/auth.py` — returns 6 or more
3. `grep -c "skip_token_hash" backend/services/auth.py` — returns 2 or more
4. `grep -c "skip_token_hash" backend/api/auth.py` — returns 3 or more
5. `logout_all` handler unchanged: still calls `revoke_all_refresh_tokens(session, current_user.id)` without skip arg
</verification>
<success_criteria>
- `revoke_all_refresh_tokens` has signature `(session, user_id, skip_token_hash: Optional[str] = None) -> int` and filters by token_hash when skip is set
- All three handlers (`change_password`, `enable_totp`, `disable_totp`) call `revoke_all_refresh_tokens` with the derived `skip_token_hash` before `session.commit()`
- All three handlers include `"sessions_revoked": revoked` in their response dict
- All three handlers include `"sessions_revoked": revoked` in their `write_audit_log` `metadata_` kwarg
- No existing tests broken (run `pytest -x -q` after changes to confirm)
</success_criteria>
<output>
Create `.planning/phases/07.1-security-session-revocation-on-privilege-change/07.1-01-SUMMARY.md` when done.
</output>
@@ -0,0 +1,226 @@
---
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>