feat(auth): add server-controlled student and admin sessions

Replaces the Phase 2A pilot ?sid= query param with an HttpOnly,
SameSite=Lax session cookie. Gateway now derives student identity from
the session (via Platform API's /api/session/whoami) instead of trusting
a client-suppliable id, and checks ownership on every proxied HTTP and
WebSocket Lab request, not just the initial navigation.

Fixes a routing bug found during testing: the fallback route was using
'does this session own a running environment' to decide whether an
unprefixed /api/* call belonged to the Lab UI or the dashboard — true in
both places. Replaced with an explicit LAB_OWNED_PATHS allowlist of the
exact unprefixed paths the existing Lab UI actually calls.

e2e-phase2a.js rewritten to use a cookie jar instead of headers/query
params, with explicit impersonation-rejection tests added. 10/10 passing.
This commit is contained in:
2026-08-18 12:22:58 -05:00
parent 6b52c9e1d2
commit ae75bb4cbe
6 changed files with 257 additions and 294 deletions
+6 -5
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { api, clearStudent } from './api'
import { api } from './api'
import styles from './Platform.module.css'
const POLL_MS = 2500
@@ -44,9 +44,10 @@ export default function StudentDashboard({ student, onLogout }) {
const result = await api.launchEnvironment()
setEnvStatus(result)
if (result.status === 'running' && result.launch_path) {
// sid is a defense-in-depth ownership check the Gateway verifies on this specific
// navigation — see docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md "Routing design."
window.location.href = `${result.launch_path}?sid=${encodeURIComponent(student.student_id)}`
// No ?sid= needed anymore (Phase 2B) — the Gateway verifies ownership from the
// rn_session cookie itself, which travels with this navigation automatically. See
// docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §3.
window.location.href = result.launch_path
}
} catch (e) {
setError(e.message)
@@ -67,7 +68,7 @@ export default function StudentDashboard({ student, onLogout }) {
}
}
const handleLogout = () => { clearStudent(); onLogout() }
const handleLogout = () => { api.logout().then(onLogout) }
const nextStage = curriculum?.stages?.find((s) => s.id === curriculum.next_stage)
const nextScenario = nextStage?.scenarios?.find((sc) => sc.status !== 'completed')