bug(security): browser-side /api/* requests miss Authorization in production → browser shows Basic-auth popup #520
Reference in New Issue
Block a user
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?
Summary
The auth model assumes every
/api/*request carries anAuthorization: Basic <base64>header. The login action sets an HttpOnlyauth_tokencookie. In dev, Vite's proxy (frontend/vite.config.ts) readsauth_tokenand injects the header on every/api/*request. In production, Caddy proxies/api/*straight to the backend with no header injection. Result:fetch('/api/...')(15+ call sites, including the documents page transcription editor and the OCR progress poller) hit the backend withoutAuthorization.EventSourceconnections (/api/notifications/stream,/api/ocr/jobs/.../progress) hit the backend withoutAuthorization.401 WWW-Authenticate: Basic realm="Realm"→ browser shows a native Basic-auth popup every time the page makes a client-side call.Why nobody caught this before
This is the first production-style deploy of #497. Local dev's Vite proxy hides the problem. The deploy smoke test only hits
/login,/, and/actuator/health— never an authenticated/api/*from the browser context.Fix
Backend
OncePerRequestFilterthat promotes theauth_tokencookie to anAuthorizationheader before Spring Security's filter chain runs. URL-decodes the cookie value (the login action URL-encodes "Basic " → "Basic%20"). Only applies if no explicitAuthorizationheader is already present.This makes the backend work the same way for:
hooks.server.tsinjects the header explicitly)No Caddy changes, no client-side code changes, no protocol changes. One filter class + one slice test.
Alternatives considered
header_up Authorization {http.request.cookie.auth_token}— fails because Caddy doesn't URL-decode the cookie value and Spring's Basic parser rejectsBasic%20YWR….WWW-Authenticateheader (custom AuthenticationEntryPoint) — hides the popup but leaves the/api/*calls actually broken; user sees empty pages instead.Discovered
Logging into the live staging stack after #514 / #517 / #518 unblocked basic auth — login lands the dashboard, then a popup fires on the first SSE/notification fetch.