---
phase: 02
slug: users-authentication
status: validated
nyquist_compliant: true
wave_0_complete: true
created: 2026-05-31
---

# Phase 02 — Validation Strategy

> Nyquist validation audit — reconstructed from PLAN/SUMMARY artifacts (State B).

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Backend framework** | pytest (pytest-asyncio, httpx.AsyncClient) |
| **Backend config** | `backend/pytest.ini` |
| **Backend quick run** | `cd backend && python -m pytest tests/test_auth_api.py tests/test_auth_totp.py tests/test_admin_api.py -v` |
| **Backend full suite** | `cd backend && python -m pytest -v` |
| **Frontend framework** | Vitest |
| **Frontend config** | `frontend/vitest.config.js` |
| **Frontend run** | `cd frontend && npx vitest run` |

---

## Per-Task Verification Map

| Task ID | Plan | Wave | Requirement | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------------|-----------|-------------------|-------------|--------|
| 02-01-T1 | 01 | 1 | AUTH-01, AUTH-02 | Argon2 hash, JWT lifecycle, BackupCode model | Unit | `pytest tests/test_task1_models_config.py tests/test_task2_auth_service.py -v` | ✅ | ✅ green |
| 02-01-T2 | 01 | 1 | AUTH-07, SEC-06 | Refresh family revocation, constant-time backup code verify | Integration | `pytest tests/test_task2_auth_service.py -v` | ✅ | ✅ green |
| 02-01-T3 | 01 | 1 | AUTH-01, AUTH-02 | get_current_user raises 401 on bad token; get_current_admin raises 403 | Integration | `pytest tests/test_auth_deps.py -v` | ✅ | ✅ green |
| 02-02-T1 | 02 | 2 | AUTH-01, AUTH-02, AUTH-04 | Register/login/refresh/logout/me/change-password endpoints | Integration | `pytest tests/test_auth_api.py -v` | ✅ | ✅ green |
| 02-02-T1 | 02 | 2 | SEC-01 | Origin validation middleware rejects cross-origin POST with 403 | Integration | `pytest tests/test_auth_api.py::test_origin_rejected -v` | ✅ | ✅ green |
| 02-02-T1 | 02 | 2 | SEC-02 | Per-account rate limit: 11th login attempt returns 429 | Integration | `pytest tests/test_auth_api.py::test_per_account_rate_limit -v` | ✅ | ✅ green |
| 02-02-T1 | 02 | 2 | **SEC-05** | CSP + X-Frame-Options + X-Content-Type-Options on all responses | Integration | `pytest tests/test_security_headers.py -v` | ✅ | ✅ green |
| 02-02-T2 | 02 | 2 | AUTH-01, AUTH-04 | useAuthStore never writes to localStorage; login() passes backup_code | Unit (Vitest) | `cd frontend && npx vitest run src/stores/__tests__/auth.test.js` | ✅ | ✅ green |
| 02-03-T1 | 03 | 3 | AUTH-03 | TOTP setup returns provisioning_uri; enable rate-limited 10/min | Integration | `pytest tests/test_auth_totp.py -v` | ✅ | ✅ green |
| 02-03-T1 | 03 | 3 | AUTH-05 | Password reset confirm returns 200 with no access_token (no auto-login) | Integration | `pytest tests/test_auth_totp.py::test_password_reset_confirm_valid_no_autologin -v` | ✅ | ✅ green |
| 02-03-T1 | 03 | 3 | AUTH-06 | logout-all revokes all refresh tokens | Integration | `pytest tests/test_auth_totp.py::test_logout_all_revokes_tokens -v` | ✅ | ✅ green |
| 02-03-T1 | 03 | 3 | **AUTH-08** | TOTP replay: same code rejected within 90-second window | Integration | `pytest tests/test_totp_replay.py -v` | ✅ | ✅ green |
| 02-03-T1 | 03 | 3 | **SEC-03** | Constant-time comparison: hmac.compare_digest used; verify_password/backup_code reject wrong inputs | Unit | `pytest tests/test_constant_time_auth.py -v` | ✅ | ✅ green |
| 02-03-T2 | 03 | 3 | AUTH-01 | PasswordStrengthBar: correct 0-4 score (length + char types) | Unit (Vitest) | `cd frontend && npx vitest run src/components/auth/__tests__/PasswordStrengthBar.test.js` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | ADMIN-01 | Admin-created users have password_must_change=True | Integration | `pytest tests/test_admin_api.py::test_create_user_sets_password_must_change -v` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | ADMIN-02 | Deactivate/reactivate user + sole-admin guard | Integration | `pytest tests/test_admin_api.py::test_deactivate_user tests/test_admin_api.py::test_cannot_deactivate_only_admin -v` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | ADMIN-03 | Admin password reset sends email via Celery; no impersonation | Integration | `pytest tests/test_admin_api.py::test_password_reset_initiates_email -v` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | ADMIN-04 | Quota update with warning when limit < used_bytes | Integration | `pytest tests/test_admin_api.py::test_quota_below_usage_warning -v` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | ADMIN-05 | AI provider/model assignment per user | Integration | `pytest tests/test_admin_api.py::test_update_ai_config -v` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | ADMIN-07 | No impersonation endpoint exists (404/422) | Integration | `pytest tests/test_admin_api.py::test_admin_impersonation_not_found -v` | ✅ | ✅ green |
| 02-04-T1 | 04 | 4 | SEC-07 | get_current_admin enforced: non-admin gets 403 | Integration | `pytest tests/test_admin_api.py::test_list_users_requires_admin -v` | ✅ | ✅ green |
| 02-04-T2 | 04 | 4 | SEC-07 | Admin responses never include password_hash | Integration | `pytest tests/test_admin_api.py::test_admin_response_no_password_hash -v` | ✅ | ✅ green |
| 02-05-T2 | 05 | 5 | ADMIN-01..03 | AdminUsersTab: onMount fetch, deactivate call, empty state | Unit (Vitest) | `cd frontend && npx vitest run src/components/admin/__tests__/AdminUsersTab.test.js` | ✅ | ✅ green |
| 02-05-T2 | 05 | 5 | ADMIN-04 | AdminQuotasTab: save call, below-usage warning displayed | Unit (Vitest) | `cd frontend && npx vitest run src/components/admin/__tests__/AdminQuotasTab.test.js` | ✅ | ✅ green |
| 02-05-T2 | 05 | 5 | ADMIN-05 | AdminAiConfigTab: save call, 1.5s Saved confirmation | Unit (Vitest) | `cd frontend && npx vitest run src/components/admin/__tests__/AdminAiConfigTab.test.js` | ✅ | ✅ green |
| 02-06-T2 | 06 | 1 | SEC-07, AUTH-01 | `requiresAdmin` guard redirects non-admin to `/`; all 4 auth routes carry `meta.layout: 'auth'` | Unit (Vitest) | `cd frontend && npx vitest run src/router/__tests__/router.guard.test.js` | ✅ | ✅ green |
| 02-06-T3a | 06 | 1 | AUTH-03 | TotpEnrollment renders `<img src="data:image/...">` QR code in verify step; no `otpauth://` link | Unit (Vitest) | `cd frontend && npx vitest run src/components/auth/__tests__/TotpEnrollment.test.js` | ✅ | ✅ green |
| 02-06-T3b | 06 | 1 | AUTH-03, AUTH-04 | SettingsAccountTab mounts all 4 sections; totp_enabled toggle shows correct 2FA state | Unit (Vitest) | `cd frontend && npx vitest run src/components/settings/__tests__/SettingsAccountTab.test.js` | ✅ | ✅ green |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| TOTP QR code renders and scans correctly | AUTH-03 | Requires a physical authenticator app (Google Auth / Authy) | 1. Register + login; 2. GET /api/auth/totp/setup; 3. Open provisioning_uri in authenticator app; 4. Verify 6-digit code is accepted by POST /api/auth/totp/enable |
| Email delivery (SMTP) | AUTH-05, ADMIN-03 | Requires live SMTP server | Configure SMTP_* env vars; trigger password reset; verify email arrives with correct reset link |
| Admin panel browser rendering | ADMIN-01..05 | Vue component visual contract | Start dev server; log in as admin; verify all three tabs render correctly per UI-SPEC |
| httpOnly cookie SameSite=Strict | SEC-01 | Browser DevTools required | Log in via browser; open DevTools → Application → Cookies; verify refresh_token is HttpOnly + SameSite=Strict |

---

## Validation Audit 2026-05-31

| Metric | Count |
|--------|-------|
| Gaps found | 6 (1 MISSING backend, 2 PARTIAL backend, 3 MISSING frontend) |
| Resolved | 6 |
| Escalated | 0 |
| New test files | 8 |
| Total tests added | 60 (14 backend + 46 frontend) |

## Validation Audit 2026-06-01

| Metric | Count |
|--------|-------|
| Gaps found | 3 (3 MISSING frontend — plan 02-06 not covered in prior audit) |
| Resolved | 3 |
| Escalated | 0 |
| New test files | 3 |
| Total tests added | 16 (router guard × 10, TotpEnrollment × 2, SettingsAccountTab × 4) |

---

## Validation Sign-Off

- [x] All tasks have automated verify or Manual-Only justification
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references (gaps filled by audit)
- [x] No watch-mode flags
- [x] `nyquist_compliant: true` set in frontmatter

**Approval:** 2026-05-31
