- Add JWT token auth to OPC WebSocket (unauthenticated → 403, per-user tracking) - Externalize Transmission RPC credentials to TRANSMISSION_USER/PASS env vars - Remove hardcoded SECRET_KEY fallback from dev compose - Rate-limit passkey register options endpoint at 5/minute - Add PDD (docs/improvement-plan.md) and sprint specs (specs/) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5.7 KiB
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:
LoginRequesthasmax_length=128on username,max_length=1024on password. Requests exceeding limits get 422 with clear field error. -
Verify by:
curl -X POST /api/auth/login -d '{"username":"'$(python -c 'print("a"*200)')'","password":"test"}'→ 422, error mentions max_length- 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_overrideandrbac_update_roleuse Pydantic models (RbacOverrideRequest,RbacUpdateRoleRequest) instead ofawait request.json(). Unknown fields are rejected. Existing RBAC functionality unchanged. -
Verify by:
- Send valid override payload → 200, override saved
- Send payload with unknown field
sidebar_links→ 422 validation error - Send payload missing required
pagesfield → 422 - Existing RBAC tests pass (
test_rbac.py,test_auth_flow.py)
-
Risk: Audit frontend
Settings.svelteto 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,deleteuse Pydantic models matching WebAuthn payload structure. Malformed payloads get 422 instead of 500. -
Verify by:
- Send valid WebAuthn registration payload → 200
- Send malformed payload (missing
responsefield) → 422 with field-level error - Existing passkey tests pass (
test_passkey.py)
-
Risk: WebAuthn payloads have specific structure — cross-reference with the
webauthnlibrary'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_challengeonly returns the challenge if the session matches. -
Verify by:
- User A requests challenge → challenge stored with user A's session ID
- User B (different session) tries to verify with user A's challenge → 400 "invalid challenge"
- User A verifies with own challenge → success
- Existing passkey tests pass
-
Risk: Need session identifier for unauthenticated users during login flow. Use server-generated
challenge_idreturned 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-logrequires admin role. Non-admin users get 403. Log collection unchanged. -
Verify by:
- Admin user requests audit log → 200, entries returned
- 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/logsrequires admin role. Non-admin users get 403. -
Verify by:
- Admin user requests security logs → 200
- 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.pyimportsjsondirectly and usesjson.dumps()instead ofopc_db.json.dumps(). -
Verify by:
grep "opc_db.json" dashboard/backend/services/agent_executor.py→ no match- 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=Falseat startup,logger.warning()fires explaining the risk.ALLOW_INSECURE_COOKIES=trueenv var suppresses the warning. -
Verify by:
- Start without
ALLOW_INSECURE_COOKIES→ warning in logs - Start with
ALLOW_INSECURE_COOKIES=true→ no warning
- Start without
-
Risk: Low — purely additive, doesn't change cookie behavior.
-
S02.8 — Add COOKIE_SECURE startup warning
Exit criteria
LoginRequestrejects 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