Files
nas-tools/specs/sprint-02-auth-hardening.md
Gan, Jimmy ddaf3c6cee security: Sprint 00 — critical security fixes (OPC WS auth, Transmission creds, SECRET_KEY, passkey rate limit)
- 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>
2026-05-03 13:31:25 +08:00

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: 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

  • 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