Session-based dashboard auth (stop storing the master key in the browser) #22
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Design/spec: Obsidian
Personal/Projects/edge-router/specs/2026-08-10-dashboard-session-auth-design.md.Problem: the dashboard stores the master API key in
localStorageand sends it asX-API-Keyon every call — an all-powerful, permanent credential sitting in the browser (XSS → exfiltration → persistent compromise).Fix (step 1 of 2; step 2 = passkeys): exchange an API key for a server-issued httpOnly session cookie. The browser never holds the master key; the JS holds no credential. Header API-key auth is unchanged for machines.
Tasks
SessionStoreonAppState(opaque 32-byte token; 60 min sliding idle + 12 h absolute cap; periodic sweep)POST /api/session(login): validate key → seter_sessioncookieHttpOnly; SameSite=Strict; Secure(when HTTPS viaIsHttps)POST /api/session/logout: invalidate + clear cookieauth_middleware: accept a valid session cookie (sliding-bump) ORX-API-Key(additive)admin_host_gate+admin_ip_middlewarebut NOTauth_middleware/api/session; delete alllocalStorage/X-API-KeyJS; logout buttonOut of scope (separate): passkeys (step 2), API-key hashing at rest (#18), CSP hardening, login rate-limiting.
Implemented in
21ef3d5.SessionStore(session.rs): opaque 32-byte token, 60 min sliding idle + 12 h absolute cap, periodic sweep. Not persisted (clears on restart).POST /api/session(login) setser_sessioncookie —HttpOnly; SameSite=Strict; Path=/,Secureonly when the request arrived over HTTPS (viaIsHttps).POST /api/session/logoutinvalidates + clears.auth_middlewareaccepts a valid session cookie (sliding-bumped) ORX-API-Key— additive; machines unchanged.admin_host_gate+admin_ip_middleware, notauth_middleware.dashboard.html: login posts/api/session; alllocalStorage/X-API-KeyJS removed — same-origin fetch carries the cookie, JS holds no credential; logout button added.Tests:
SessionStore(idle expiry, sliding bump, absolute cap, sweep, cookie parse+flags); end-to-end login → cookie → authenticated admin call → logout → 401; wrong key → 401 no cookie. Full suite green (180), clippy/fmt clean. The dashboard isinclude_str!-embedded in the binary, so deploying the image updates it.Verification needed on deploy (inherently not unit-testable): the browser flow — login box submits, gate hides on success, logout re-shows it, a reload with a live cookie skips the login box, and
localStorageis empty (DevTools → Application). Also confirm the realSet-Cookie— noteSecuretracks the internal hop (edge-router's TLS binding), so if Cloudflare terminates TLS and forwards plain HTTP, the cookie may ship withoutSecureeven though the browser↔CF leg is HTTPS.Step 1 of 2 complete. Step 2 (passkeys) builds on this. Closing.