Files
kite/.planning/phases/02-users-authentication/02-VERIFICATION.md
T

24 KiB
Raw Blame History

phase, verified, status, score, overrides_applied, re_verification, deferred, human_verification
phase verified status score overrides_applied re_verification deferred human_verification
02-users-authentication 2026-06-01T14:35:00Z human_needed 6/6 0
previous_status previous_score gaps_closed gaps_remaining open_findings regressions
gaps_found 4/5
Admin can create a new user via POST /api/admin/users without HTTP 500 (session.flush() confirmed, regression test passes)
Auth/login pages show AuthLayout only — App.vue now layout-aware via route.meta.layout conditional
After logout the sidebar is gone — same App.vue v-if fix covers the logged-out state
Non-admin user navigating to /admin is redirected to / — requiresAdmin guard in beforeEach wired
TOTP enrollment shows scannable QR image — qrcode library installed, img tag renders QR from QRCode.toDataURL
TOTP enrollment accessible from Account tab in /settings — SettingsAccountTab.vue created and wired
SC5 (admin JWT returns 403 on document content) — deferred to Phase 3 per D-07 CONTEXT.md decision
CR-01: change_password does not revoke active sessions (CLAUDE.md line 153 — security invariant)
CR-02: disable_totp does not revoke active sessions (CLAUDE.md line 153 — security invariant)
CR-03: ConfirmBlock.vue has no named slot — #confirm-button in SettingsAccountTab is dead (spinner/guard never activates)
WR-01: decodeURIComponent on query param in SettingsView.vue has no error handling — URIError on malformed %encoding
WR-02: TOTP verify code button re-enables during 800ms success flash — double-submission possible
WR-03: Password error routing uses fragile string-matching on raw API messages
WR-04: topicsStore.fetchTopics() fires unconditionally on every page load including auth pages
truth addressed_in evidence
Attempting to access document content via an admin JWT returns 403 Phase 3 Phase 3 goal: Document Migration and Multi-User Isolation. CONTEXT.md D-07: existing /api/documents stays public in Phase 2; gains get_current_user guards in Phase 3. REQUIREMENTS.md traceability: SEC-04 mapped to Phase 3.
test expected why_human
TOTP enrollment end-to-end User navigates to /settings, clicks Account tab, sees TotpEnrollment component. In setup step: QR image renders (not a text link). User scans QR with authenticator app. In verify step: user enters 6-digit code. In backup-codes step: 10 codes displayed in 2-column grid with Copy All button and acknowledgment checkbox gating Enable 2FA. After enabling: account shows 2FA active; next login requires TOTP code. Multi-step flow requires authenticator app; QR image rendering requires visual confirmation; backup-code acknowledgment gate requires UI interaction
test expected why_human
Password reset email delivery User triggers /password-reset for a real email account. Email arrives with correct signed link. Link expires after 1 hour. Following the link and submitting a new strong password returns success message with no auto-login. User must go to /login and pass TOTP gate if 2FA was enabled. Requires SMTP/Celery infrastructure running and actual email receipt; anti-enumeration 202 response cannot confirm dispatch
test expected why_human
Sign out all devices User clicks Sign out all devices in /settings Account tab. ConfirmBlock appears. On confirm: all sessions revoked, current browser redirected to /login. A second browser tab's next authenticated request fails with 401. Multi-session testing requires two live sessions; refresh token family invalidation requires browser-level verification
test expected why_human
Admin panel role visibility and CRUD Regular user does not see Admin link in sidebar and cannot navigate to /admin (redirected to /). Admin user sees Admin link with shield icon; can navigate Users/Quotas/AI Config tabs; can create a test user (no HTTP 500); can deactivate a user with inline confirmation showing correct email. Visual rendering, role-conditional DOM, and inline confirmation UX require browser interaction
test expected why_human
CR-01 / CR-02: Session revocation on password change and TOTP disable After successfully changing password in Account tab: current session is invalidated and user is redirected to /login (or receives a clear sign-out prompt). Any other active refresh tokens are revoked. Same behavior after disabling TOTP. A previously-valid refresh cookie must fail with 401 after the change. Requires confirming backend revocation behavior with live sessions; current code does NOT revoke sessions (CR-01/CR-02 are open code-review blockers — this test is expected to FAIL until the backend fix is applied)

Phase 2: Users & Authentication — Verification Report (Re-Verification after Plan 06 Gap Closure)

Phase Goal: Users can register, log in (with optional TOTP 2FA), reset their password, and sign out all active sessions; admins can manage user accounts and assign AI providers — all enforced by a complete FastAPI dependency chain.

Verified: 2026-06-01T14:35:00Z Status: HUMAN NEEDED (all automated checks pass; 5 items require human testing; 2 security invariants from CLAUDE.md require developer resolution) Re-verification: Yes — after Plan 06 gap closure (5 UAT gaps closed)


Goal Achievement

Observable Truths (Success Criteria from Plan 06 must_haves)

# Truth Status Evidence
T1 Admin can create a new user via POST /api/admin/users without HTTP 500 VERIFIED await session.flush() at admin.py:247 (before write_audit_log()); test_create_user_writes_audit_log passes (1 passed, 2.23s)
T2 Login, register, and password-reset pages show AuthLayout only — no sidebar, no user identity footer VERIFIED App.vue line 2: <AuthLayout v-if="route.meta.layout === 'auth'" />; all 4 auth routes have meta: { public: true, layout: 'auth' } in router/index.js (4 grep matches at lines 22, 27, 32, 37)
T3 After logout the sidebar is gone — the user lands on the login page with AuthLayout VERIFIED Same App.vue v-if fix covers logged-out state; /login has layout: 'auth' meta so AuthLayout renders, not app shell
T4 Non-admin user navigating to /admin is redirected to / VERIFIED router/index.js:91-93: if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') return { path: '/' }; /admin has meta: { requiresAdmin: true } at line 42
T5 TOTP enrollment step 1 shows a scannable QR image, not a text link VERIFIED TotpEnrollment.vue:111 import QRCode from 'qrcode'; line 120 const qrDataUrl = ref(''); line 136 qrDataUrl.value = await QRCode.toDataURL(qrUri.value, ...); line 34 <img v-if="qrDataUrl" :src="qrDataUrl" alt="TOTP QR code" ...>
T6 TOTP enrollment option is accessible from a tab within /settings (Account tab) VERIFIED SettingsView.vue:92 imports SettingsAccountTab; line 100 { id: 'account', label: 'Account' } in tabs array; line 52 <SettingsAccountTab v-if="activeTab === 'account'" />; SettingsAccountTab.vue contains TotpEnrollment component at line 63

Score: 6/6 truths verified


Deferred Items (from Initial Verification — SC5)

Items not yet met but explicitly addressed in later milestone phases.

# Item Addressed In Evidence
1 Attempting to access document content via an admin JWT returns 403 Phase 3 Phase 3 goal: "Document Migration and Multi-User Isolation." CONTEXT.md D-07: /api/documents, /api/topics, /api/settings stay public in Phase 2; gain get_current_user guards in Phase 3. REQUIREMENTS.md: SEC-04 mapped to Phase 3. The admin panel API (/api/admin/*) correctly enforces get_current_admin on all endpoints.

Required Artifacts

Artifact Expected Status Details
backend/api/admin.py await session.flush() before write_audit_log() in create_user VERIFIED Line 247: await session.flush() # persist User + Quota before audit_log FK references them
backend/tests/test_admin_api.py test_create_user_writes_audit_log regression test VERIFIED Line 145; test passes (confirmed by pytest run)
frontend/src/router/index.js meta: { layout: 'auth' } on 4 auth routes; meta: { requiresAdmin: true } on /admin; beforeEach role check VERIFIED 4 routes at lines 22, 27, 32, 37; /admin at line 42; beforeEach check at lines 91-93
frontend/src/App.vue Layout-aware root — AuthLayout for auth routes, app shell for all others VERIFIED Line 2: <AuthLayout v-if="route.meta.layout === 'auth'" />; line 3: <div v-else ...> with AppSidebar + router-view
frontend/src/views/SettingsView.vue Account tab rendering SettingsAccountTab VERIFIED Line 52: <SettingsAccountTab v-if="activeTab === 'account'" />; line 92: import; line 100: tab array entry
frontend/src/components/settings/SettingsAccountTab.vue Full AccountView content (2FA, change password, sign-out-all) VERIFIED 253 lines; 4 sections (Account info, 2FA/TotpEnrollment, Change password, Sessions); all script setup logic ported
frontend/src/components/auth/TotpEnrollment.vue QR image via qrcode library (no <a href="otpauth://...">) VERIFIED QRCode imported line 111; qrDataUrl ref line 120; toDataURL call line 136; img tag line 34
frontend/package.json "qrcode" in dependencies VERIFIED "qrcode": "^1.5.4" in dependencies section (not devDependencies)

From To Via Status Details
frontend/src/App.vue frontend/src/layouts/AuthLayout.vue v-if route.meta.layout === 'auth' WIRED Line 2 template; line 15 import
frontend/src/router/index.js frontend/src/stores/auth.js beforeEach reads authStore.user?.role WIRED Line 91: authStore.user?.role !== 'admin'
frontend/src/components/auth/TotpEnrollment.vue qrcode npm package import QRCode from 'qrcode'; QRCode.toDataURL(qrUri.value) WIRED Lines 111, 136
frontend/src/views/SettingsView.vue frontend/src/components/settings/SettingsAccountTab.vue import + v-if render WIRED Lines 52, 92, 100
frontend/src/router/index.js /settings redirect for /account { path: '/account', redirect: '/settings' } WIRED Line 41

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
TotpEnrollment.vue qrDataUrl QRCode.toDataURL(qrUri.value) after api.totpSetup() returns provisioning_uri Yes — real otpauth:// URI from backend, converted to PNG data URL FLOWING
SettingsAccountTab.vue authStore.user.totp_enabled Pinia auth store (populated from login response) Yes — real DB-backed value FLOWING
SettingsAccountTab.vue authStore.user.email, .handle, .role Pinia auth store /api/auth/me response Yes — real user data FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
Admin create_user regression test python3 -m pytest tests/test_admin_api.py::test_create_user_writes_audit_log -v 1 passed, 2.23s PASS
Frontend build npm run build 156 modules, exit 0, built in 1.82s PASS
Frontend test suite npm test 107/107 passed (11 test files) PASS
4 auth routes have meta.layout:'auth' grep -c "layout.*auth" router/index.js 4 matches PASS
/admin has meta.requiresAdmin grep -n "requiresAdmin" router/index.js Line 42 (route def) + line 91 (beforeEach check) PASS
qrcode in package.json dependencies grep "qrcode" package.json "qrcode": "^1.5.4" PASS
QRCode.toDataURL + img tag in TotpEnrollment grep -n "toDataURL|qrDataUrl|img.*qr" TotpEnrollment.vue Lines 34, 120, 136 PASS
SettingsAccountTab imported and rendered in SettingsView grep -n "account|SettingsAccountTab" SettingsView.vue Lines 52, 92, 100 PASS
No localStorage in auth store grep -c "localStorage" frontend/src/stores/auth.js 0 (from initial verification) PASS

Requirements Coverage

Requirement Plan Description Status Evidence
AUTH-03 02-03, 02-06 TOTP enrollment with 810 backup codes acknowledged before activation SATISFIED generate_backup_codes(10) + BackupCodesDisplay acknowledgment gate (initial verification); QR image now rendered via qrcode library (plan 06 fix)
AUTH-04 02-02 Login via TOTP code or single-use backup code SATISFIED verify_totp() + verify_backup_code() paths in login handler; used_at set on use
AUTH-05 02-03 Password reset via email; no auto-login; returns to TOTP gate SATISFIED Confirm endpoint returns 200 + message, no tokens; revoke_all_refresh_tokens() called
SEC-01 02-02 CSRF protection (SameSite=Strict + Origin validation) SATISFIED OriginValidationMiddleware + SameSite=Strict on refresh cookie
SEC-03 02-01 Parameterized queries / ORM SATISFIED All DB ops via SQLAlchemy ORM; zero raw string interpolation
ADMIN-01 02-04, 02-06 Admin creates user with temp password, password_must_change=True SATISFIED POST /api/admin/users sets password_must_change=True; HTTP 500 fixed via session.flush() at admin.py:247
ADMIN-07 02-04 Admin impersonation explicitly excluded SATISFIED No impersonation endpoint; test_admin_impersonation_not_found asserts 404/422

Open Code Review Findings (from 02-REVIEW.md)

These findings were surfaced by the code reviewer after plan 06 execution and are NOT yet fixed. They represent security invariants and UX gaps that require developer attention before Phase 2 can be considered fully resolved.

CR-01 — BLOCKER: Password change does not revoke active sessions

File: backend/api/auth.py line 520 (change_password endpoint) Issue: CLAUDE.md line 153 requires: "Password change, TOTP enroll/revoke, and account deactivation immediately revoke all active sessions." The change_password handler updates password_hash and commits but does not call revoke_all_refresh_tokens(). An attacker with a previously-stolen refresh cookie retains full access for up to 30 days after the victim changes their password. Fix required: Add await auth_service.revoke_all_refresh_tokens(session, current_user.id) after session.commit() in the change_password handler. Also redirect the current session to /login in the frontend changePassword() success path.

CR-02 — BLOCKER: TOTP disable does not revoke active sessions

File: backend/api/auth.py line 641 (disable_totp endpoint) Issue: Same CLAUDE.md line 153 requirement. The disable_totp handler clears totp_secret, sets totp_enabled=False, and deletes backup codes but does not revoke any refresh tokens. An attacker can downgrade account security and both the attacker and any stolen sessions remain valid. Fix required: Add await session.execute(delete(RefreshToken).where(RefreshToken.user_id == current_user.id)) + await session.commit() in the disable_totp handler after backup code deletion.

CR-03 — WARNING: Dead #confirm-button slot in SettingsAccountTab removes spinner/guard on sign-out-all

File: frontend/src/components/settings/SettingsAccountTab.vue lines 149161 Issue: ConfirmBlock.vue defines no named slot. The <template #confirm-button> block is silently ignored by Vue; the custom button with :disabled="signingOutAll" and <AppSpinner> never renders. Double-invocation of signOutAll() is possible with no spinner feedback. Fix required: Either add <slot name="confirm-button"> fallback in ConfirmBlock.vue, or guard with if (signingOutAll.value) return at the top of signOutAll().

WR-01 — WARNING: URIError crash path in SettingsView.vue onMounted

File: frontend/src/views/SettingsView.vuedecodeURIComponent(errorMsg) in onMounted Issue: No try/catch. Malformed cloud_error query param (e.g. lone %) throws URIError; user sees blank cloud tab with no error message. Fix required: Wrap in try/catch, fall back to raw errorMsg value.

WR-02 — WARNING: TOTP verify code button re-enables during 800ms success flash

File: frontend/src/components/auth/TotpEnrollment.vue Issue: loading is cleared in finally before the 800ms timeout fires; verifyCode not cleared on success. Button re-enables, allowing duplicate api.totpEnable() call. Backend replay prevention should reject it but UX is broken. Fix required: Clear verifyCode.value = '' immediately after api.totpEnable() succeeds.

WR-03 — WARNING: Fragile string-matching for password error routing

File: frontend/src/components/settings/SettingsAccountTab.vue Issue: passwordError.includes('Current') routes errors to field-level vs. form-level display. Unexpected API messages containing "Current" will be misrouted. Fix required: Use separate refs for field-level and form-level errors.

WR-04 — WARNING: topicsStore.fetchTopics() fires on auth page loads

File: frontend/src/App.vue line 20 Issue: onMounted(() => topicsStore.fetchTopics()) runs on every route including /login, /register, /password-reset. The backend returns 401; the API client attempts a token refresh (which fails); auth store clears its already-null accessToken. Spurious 401 + failed refresh on every auth page load. Fix required: Guard the call behind authStore.accessToken check, or move it to the authenticated app shell.

IN-01 — INFO: qrcode uses caret range instead of exact pin

File: frontend/package.json Issue: "qrcode": "^1.5.4" — CLAUDE.md requires exact version pinning for security-critical packages. Fix recommended: Pin to "qrcode": "1.5.4".


Anti-Patterns Found

File Line Pattern Severity Impact
backend/api/auth.py 520 change_password commits without revoke_all_refresh_tokens() BLOCKER Active sessions survive password change — CLAUDE.md line 153 security invariant violated
backend/api/auth.py 641 disable_totp commits without revoke_all_refresh_tokens() BLOCKER Active sessions survive TOTP disable — CLAUDE.md line 153 security invariant violated
frontend/src/components/settings/SettingsAccountTab.vue 149161 <template #confirm-button> renders into nonexistent slot — dead code WARNING Spinner and double-click guard never activate on sign-out-all
frontend/src/App.vue 20 topicsStore.fetchTopics() unconditional on mount WARNING Spurious 401 + failed token refresh on every auth page load
frontend/src/views/SettingsView.vue onMounted decodeURIComponent() without try/catch WARNING URIError crash on malformed cloud_error query param
frontend/src/components/auth/TotpEnrollment.vue verify step verifyCode not cleared on success before 800ms timeout WARNING Button re-enables during flash window — double-submission possible

No TBD/FIXME/XXX markers found in Phase 2 plan-06 deliverable files.


Human Verification Required

5 items need human testing:

1. TOTP Enrollment End-to-End

Test: Log in as a user, navigate to /settings, click the Account tab. Find the "Two-factor authentication" section. Click to set up 2FA. Verify a scannable QR image (not a text link) appears. Scan the QR with an authenticator app (Google Authenticator, Authy, etc.). Enter the 6-digit code. Verify the backup codes screen shows 10 codes in a 2-column grid with a Copy All button and acknowledgment checkbox. Check the acknowledgment box. Click Enable 2FA. Confirm account shows 2FA active. Log out and log back in — confirm TOTP step is required.

Expected: QR image renders (no <a href="otpauth://..."> link visible). 10 backup codes displayed. Acknowledgment checkbox gates Enable 2FA button. After enabling: login requires TOTP code or backup code.

Why human: Multi-step flow requires a real authenticator app; QR image rendering requires visual confirmation; the backup-code gate requires UI interaction.

2. Password Reset Email Delivery

Test: Navigate to /password-reset. Enter a valid email address. Verify the page always shows a generic "check your email" message (no enumeration of valid/invalid email). Check the inbox. Follow the reset link. Set a new strong password. Verify no auto-login after reset. Navigate to /login and log in with the new password. Confirm TOTP gate appears if 2FA was enabled.

Expected: Email arrives with correct signed link. Link expires after 1 hour. Successful reset shows "Password updated. Please sign in." with no tokens issued. TOTP gate on next login if enrolled.

Why human: Requires SMTP infrastructure running and actual email receipt. Anti-enumeration means the 202 response alone cannot confirm email dispatch.

3. Sign Out All Devices

Test: Log in from two browser tabs or browsers. In Tab 1, go to /settings > Account tab > Sessions. Click "Sign out all devices." Verify a ConfirmBlock appears. On confirm: Tab 1 redirects to /login. In Tab 2, make an authenticated request (e.g., navigate or refresh). Verify Tab 2 is redirected to /login.

Expected: Both tabs lose access after "sign out all devices." Reusing a revoked refresh token causes family revocation.

Why human: Multi-session behavior requires two live sessions; refresh token reuse detection requires timing across requests.

4. Admin Panel Role Visibility and CRUD

Test: Log in as a regular user. Verify the Admin link is NOT visible in the sidebar. Navigate to /admin directly. Verify redirect to /. Log in as admin. Verify "Admin" link with shield icon appears in sidebar. Navigate to /admin. Test: (a) Users tab — list all users; (b) Create a new test user — verify HTTP 200/201 and the user appears in the table (no HTTP 500); (c) Deactivate the test user — verify inline confirmation shows correct email; (d) Quotas tab — view/adjust a quota; (e) AI Config tab — change a provider and verify save flash.

Expected: Non-admin users blocked from /admin (redirected to /). Admin CRUD operations work. No HTTP 500 on user creation.

Why human: Visual rendering, role-conditional DOM, and inline confirmation UX require browser interaction.

5. CR-01/CR-02: Session Revocation on Password Change and TOTP Disable (EXPECTED FAIL until fix applied)

Test: In Browser 1, log in. In Browser 2, log in as the same user. In Browser 1: change the password (Settings > Account tab). In Browser 2: attempt any authenticated action (e.g., navigate to /settings). Verify Browser 2 is redirected to /login. Repeat: In Browser 1, disable TOTP (if enrolled). In Browser 2: verify session is invalidated.

Expected: Both Browser 2 sessions are invalidated immediately after password change or TOTP disable in Browser 1.

Why human: Requires confirming backend revocation with live sessions. NOTE: This test is expected to FAIL in the current codebase. CR-01 and CR-02 are confirmed open — neither change_password nor disable_totp in backend/api/auth.py calls revoke_all_refresh_tokens(). This is a CLAUDE.md line 153 security invariant violation. A fix plan is required before Phase 2 can be marked fully complete.


Gaps Summary

0 automated gaps blocking phase goal. All 6 plan-06 must-have truths are VERIFIED. The SC5 gap from the initial verification (admin JWT on documents) is deferred to Phase 3 per D-07 and has been removed from blocking status.

2 SECURITY BLOCKERS from code review (CR-01, CR-02) — these are CLAUDE.md security invariant violations requiring a fix plan:

  • CR-01: change_password does not revoke active sessions (backend/api/auth.py:520)
  • CR-02: disable_totp does not revoke active sessions (backend/api/auth.py:641)

These are not UAT-detectable bugs — they are security architecture violations. A gap-closure plan is required targeting only these two backend endpoints. The fixes are each 35 lines (call revoke_all_refresh_tokens() after commit). The frontend should also redirect to /login after a successful password change.

4 WARNINGS from code review (WR-01 through WR-04) — lower priority UX and robustness issues not blocking the phase goal but recommended for resolution before Phase 3 begins.


Verified: 2026-06-01T14:35:00Z Re-verification: Yes — after Plan 06 gap closure Verifier: Claude (gsd-verifier)