# Sprint 02 — Auth Hardening **Depends on:** S00, S01 (need stable environment to test auth changes against) **Duration:** ~10h **Goal:** Add Pydantic validation to raw-JSON endpoints, bind passkey challenges to sessions, gate sensitive log endpoints behind admin role, fix fragile cross-module patterns. --- ## S02.1 — Add max_length to LoginRequest - **Files:** `dashboard/backend/routers/auth.py:22-25` - **Estimate:** 0.5h - **Done means:** `LoginRequest` has `max_length=128` on username, `max_length=1024` on password. Requests exceeding limits get 422 with clear field error. - **Verify by:** 1. `curl -X POST /api/auth/login -d '{"username":"'$(python -c 'print("a"*200)')'","password":"test"}'` → 422, error mentions max_length 2. Normal login with valid credentials → 200 - **Risk:** None. Pydantic validation happens before password hashing. - [ ] S02.1 — Add max_length to LoginRequest ## S02.2 — Add Pydantic models to RBAC override endpoints - **Files:** `dashboard/backend/routers/auth.py:242-259,303-326` - **Estimate:** 2h - **Done means:** `rbac_set_override` and `rbac_update_role` use Pydantic models (`RbacOverrideRequest`, `RbacUpdateRoleRequest`) instead of `await request.json()`. Unknown fields are rejected. Existing RBAC functionality unchanged. - **Verify by:** 1. Send valid override payload → 200, override saved 2. Send payload with unknown field `sidebar_links` → 422 validation error 3. Send payload missing required `pages` field → 422 4. Existing RBAC tests pass (`test_rbac.py`, `test_auth_flow.py`) - **Risk:** Audit frontend `Settings.svelte` to confirm it only sends expected fields. - [ ] S02.2 — Add Pydantic models to RBAC override endpoints ## S02.3 — Add Pydantic models to passkey endpoints - **Files:** `dashboard/backend/routers/passkey.py:78,127,188` - **Estimate:** 2h - **Done means:** `register_verify`, `login_verify`, `delete` use Pydantic models matching WebAuthn payload structure. Malformed payloads get 422 instead of 500. - **Verify by:** 1. Send valid WebAuthn registration payload → 200 2. Send malformed payload (missing `response` field) → 422 with field-level error 3. Existing passkey tests pass (`test_passkey.py`) - **Risk:** WebAuthn payloads have specific structure — cross-reference with the `webauthn` library's expected types. - [ ] S02.3 — Add Pydantic models to passkey endpoints ## S02.4 — Bind passkey challenges to sessions - **Files:** `dashboard/backend/routers/passkey.py:29-55` - **Estimate:** 1.5h - **Done means:** Passkey challenges are stored keyed by a session-bound identifier (not globally). `_get_challenge` only returns the challenge if the session matches. - **Verify by:** 1. User A requests challenge → challenge stored with user A's session ID 2. User B (different session) tries to verify with user A's challenge → 400 "invalid challenge" 3. User A verifies with own challenge → success 4. Existing passkey tests pass - **Risk:** Need session identifier for unauthenticated users during login flow. Use server-generated `challenge_id` returned in options, required in verify — simplest and stateless. - [ ] S02.4 — Bind passkey challenges to sessions ## S02.5 — Gate audit log endpoint behind admin role - **Files:** `dashboard/backend/routers/system.py:82-116` - **Estimate:** 1h - **Done means:** `/api/system/audit-log` requires admin role. Non-admin users get 403. Log collection unchanged. - **Verify by:** 1. Admin user requests audit log → 200, entries returned 2. Non-admin user requests audit log → 403 - **Risk:** Frontend Security page must handle 403 gracefully if current user isn't admin. - [ ] S02.5 — Gate audit log endpoint behind admin role ## S02.6 — Gate security log endpoint behind admin role - **Files:** `dashboard/backend/routers/security.py:198` - **Estimate:** 1h - **Done means:** `/api/security/logs` requires admin role. Non-admin users get 403. - **Verify by:** 1. Admin user requests security logs → 200 2. Non-admin user requests security logs → 403 - **Risk:** Same as S02.5 — frontend must handle 403 gracefully. - [ ] S02.6 — Gate security log endpoint behind admin role ## S02.7 — Fix fragile opc_db.json.dumps() pattern - **Files:** `dashboard/backend/services/agent_executor.py:273,307` - **Estimate:** 0.5h - **Done means:** `agent_executor.py` imports `json` directly and uses `json.dumps()` instead of `opc_db.json.dumps()`. - **Verify by:** 1. `grep "opc_db.json" dashboard/backend/services/agent_executor.py` → no match 2. OPC task creation and agent execution still work end-to-end - **Risk:** None — pure refactor. - [ ] S02.7 — Fix fragile opc_db.json.dumps() pattern ## S02.8 — Add COOKIE_SECURE startup warning - **Files:** `dashboard/backend/auth_service.py:23`, `dashboard/backend/main.py` - **Estimate:** 0.5h - **Done means:** If `COOKIE_SECURE=False` at startup, `logger.warning()` fires explaining the risk. `ALLOW_INSECURE_COOKIES=true` env var suppresses the warning. - **Verify by:** 1. Start without `ALLOW_INSECURE_COOKIES` → warning in logs 2. Start with `ALLOW_INSECURE_COOKIES=true` → no warning - **Risk:** Low — purely additive, doesn't change cookie behavior. - [ ] S02.8 — Add COOKIE_SECURE startup warning --- ## Exit criteria - [ ] `LoginRequest` rejects usernames > 128 chars with 422 - [ ] RBAC endpoints reject unknown fields with 422 - [ ] Passkey endpoints reject malformed payloads with 422 - [ ] Passkey challenges are session-bound (cross-session replay fails) - [ ] Audit log and security log endpoints return 403 for non-admin - [ ] `grep "opc_db.json" agent_executor.py` → no match - [ ] COOKIE_SECURE warning fires at startup unless suppressed - [ ] All backend tests pass - [ ] All frontend tests pass