fix: add Web UI auth with token persistence on page navigation#35
fix: add Web UI auth with token persistence on page navigation#35LuisErlacher wants to merge 3 commits intodevfrom
Conversation
Auth token was lost on page navigation because no auth system existed in the React frontend. Add optional password-based auth gated by WEB_UI_PASSWORD env var with sync localStorage token restoration to prevent redirect-on-refresh. Changes: - Server: auth middleware on /api/* with HMAC-derived Bearer token validation - Server: POST /api/auth/login and GET /api/auth/status endpoints - Frontend: AuthContext with sync localStorage init (prevents race condition) - Frontend: LoginPage with redirect-back-to-origin support - Frontend: RequireAuth wrapper with loading spinner in App.tsx - Frontend: Bearer token attachment in fetchJSON, 401 redirect to /login - SSE streams, health check, and webhooks remain unauthenticated Fixes #34 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Comprehensive PR ReviewPR: #35 — feat: add optional Web UI auth with token persistence SummaryThe core auth implementation is sound — HMAC-derived stateless tokens, synchronous localStorage init to prevent redirect flashes, SSE path exemption, and a clean opt-in feature flag. Three areas need attention before merge: the new routes bypass the project-mandated OpenAPI registration pattern, the Bearer token middleware uses non-timing-safe comparison (inconsistent with the login route), and the entire server-side auth logic has zero test coverage. Verdict:
🔴 Critical IssuesMissing tests for auth middleware bypass paths📍 The middleware has three bypass rules ( View suggested test file// packages/server/src/routes/api.auth.test.ts (new bun test batch)
describe("Auth middleware — WEB_UI_PASSWORD set", () => {
beforeAll(() => { process.env.WEB_UI_PASSWORD = "test-secret"; });
afterAll(() => { delete process.env.WEB_UI_PASSWORD; });
test("GET /api/health is accessible without token", async () => {
expect((await makeApp().request("/api/health")).status).toBe(200);
});
test("GET /api/auth/status is accessible without token", async () => {
expect((await makeApp().request("/api/auth/status")).status).toBe(200);
});
test("GET /api/stream/:id is accessible without token", async () => {
expect((await makeApp().request("/api/stream/some-id")).status).not.toBe(401);
});
test("GET /api/conversations returns 401 without token", async () => {
expect((await makeApp().request("/api/conversations")).status).toBe(401);
});
});🟠 High Issues1. Auth routes bypass
|
- Migrate auth routes to registerOpenApiRoute/createRoute per CLAUDE.md - Fix timing-safe token comparison in auth middleware (was using !=== string equality) - Fix timingSafeEqual length short-circuit by hashing both passwords with HMAC first - Fix fail-open auth status error handling (now fails-closed: defaults to enabled) - Improve login error messages to distinguish server errors from wrong password - Add warn logging for failed auth attempts (middleware + login route) - Replace window.location.href hard redirect with CustomEvent dispatch (no full reload) - Export AUTH_TOKEN_KEY from AuthContext and import it in api.ts (single source of truth) - Add api.auth.test.ts covering middleware bypass paths, login endpoint, and auth status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix Report: PR #35 — Auth Token PersistenceDate: 2026-04-13 All findings from the consolidated review have been addressed. Details below. CRITICAL FixedAuth middleware bypass paths tested (
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prefers incoming auth model (password-based with HMAC tokens) over HEAD's JWT register/login model. Fixes token persistence on page navigation by reading localStorage synchronously in AuthContext. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
WEB_UI_PASSWORDenv var (disabled by default — zero impact on existing deployments)fetchJSON; redirects to/loginon 401Changes
Server (
@archon/server)packages/server/src/routes/schemas/auth.schemas.ts— NEW: Zod schemas for login body and auth statuspackages/server/src/routes/api.ts— Auth middleware on/api/*(exempts/api/health,/api/auth/*,/api/stream/*);POST /api/auth/login+GET /api/auth/statusroutes; timing-safe password comparison viatimingSafeEqualWeb UI (
@archon/web)packages/web/src/contexts/AuthContext.tsx— NEW:AuthProviderreads token from localStorage synchronously inuseStateinitializer (mirrorsProjectContext.tsxpattern);isInitializingspinner guard for one-time/api/auth/statusfetchpackages/web/src/routes/LoginPage.tsx— NEW: Login form; redirects back to the originally requested page after loginpackages/web/src/App.tsx— Wraps routes withAuthProvider+RequireAuth; adds/loginroute; shows spinner while auth initializespackages/web/src/lib/api.ts—fetchJSONattachesAuthorization: Bearer <token>on every request; redirects to/loginon 401Docs
.env.example— DocumentsWEB_UI_PASSWORDenv varValidation
All checks passed (
bun run validate):Test scenarios
WEB_UI_PASSWORDset → all pages load normally, no login promptWEB_UI_PASSWORD=testpass→/workflowsredirects to/login/workflows→ refresh → stays on/workflows✅http://localhost:5173/workflowsin address bar → shows workflows ✅/loginNotes
isAuthenticatedis alwaystrue— existing behavior is fully preserved/api/stream/*) are exempted from token auth sinceEventSourcecannot send custom headers/webhooks/*are unaffected (they useWEBHOOK_SECRETHMAC auth)Fixes #34