Browser fetches must never receive a Basic challenge (global popup hazard) + wizard hands over to /setup #168

Closed
opened 2026-08-09 23:53:06 +00:00 by Cordy · 1 comment
Owner

Follow-up to #166, raised by Nikola after the v0.5.6 retest: is the native sign-in popup hazard global? Yes.

Diagnosis. PasswordLogin.Challenge detects browsers by Accept: text/html — which only navigations send. Any fetch()/XHR from the app (bell poll, file list, settings) that arrives unauthenticated falls through to the inner provider's WWW-Authenticate: Basic challenge, and browsers respond to that header on fetch responses with their NATIVE sign-in dialog over the open page. This is not local/LDAP-only: the dogfood's own log shows unauthenticated /api/v1/notifications 401s with 24-byte bodies ("authentication required" — a Basic challenger, reached via the Multi provider's app-password arm in OIDC mode). An expired session with the app open pops the dialog on every instance.

Fix, at the definition rather than per call site (the #153 sentinel lesson applied to auth): in auth.Middleware, before delegating to any provider's Challenge — if the request carries Sec-Fetch-Mode other than navigate, answer plain 401 {"error":"unauthenticated"} with no challenge header. Every modern browser sends Sec-Fetch-* on every request; no WebDAV client (Finder, rclone, Cyberduck) or API tool does. Consequences: browser fetches can never trigger the popup in ANY auth mode, navigations still get each provider's challenge (branded /login redirect, IdP redirect), WebDAV keeps plain Basic untouched. Pre-2023 browsers without Sec-Fetch-Mode degrade to today's behavior.

Also: the wizard's restart handover goes to /setup instead of /login — on a first run the operator's next step is creating the admin account, and /setup self-redirects to /login when first-run is not pending (or the auth middleware's navigation redirect does), so the non-first-run case stays correct with no extra probe (probing a status endpoint from a pre-auth page is exactly what #166 removed).

Follow-up to #166, raised by Nikola after the v0.5.6 retest: is the native sign-in popup hazard global? **Yes.** **Diagnosis.** `PasswordLogin.Challenge` detects browsers by `Accept: text/html` — which only navigations send. Any `fetch()`/XHR from the app (bell poll, file list, settings) that arrives unauthenticated falls through to the inner provider's `WWW-Authenticate: Basic` challenge, and browsers respond to that header on fetch responses with their NATIVE sign-in dialog over the open page. This is not local/LDAP-only: the dogfood's own log shows unauthenticated `/api/v1/notifications` 401s with 24-byte bodies (`"authentication required"` — a Basic challenger, reached via the Multi provider's app-password arm in OIDC mode). An expired session with the app open pops the dialog on every instance. **Fix, at the definition rather than per call site (the #153 sentinel lesson applied to auth):** in `auth.Middleware`, before delegating to any provider's `Challenge` — if the request carries `Sec-Fetch-Mode` other than `navigate`, answer plain `401 {"error":"unauthenticated"}` with no challenge header. Every modern browser sends `Sec-Fetch-*` on every request; no WebDAV client (Finder, rclone, Cyberduck) or API tool does. Consequences: browser fetches can never trigger the popup in ANY auth mode, navigations still get each provider's challenge (branded `/login` redirect, IdP redirect), WebDAV keeps plain Basic untouched. Pre-2023 browsers without `Sec-Fetch-Mode` degrade to today's behavior. **Also:** the wizard's restart handover goes to `/setup` instead of `/login` — on a first run the operator's next step is creating the admin account, and `/setup` self-redirects to `/login` when first-run is not pending (or the auth middleware's navigation redirect does), so the non-first-run case stays correct with no extra probe (probing a status endpoint from a pre-auth page is exactly what #166 removed).
Author
Owner

Shipped in v0.5.7 (PR #169), live on both instances.

The chokepoint fix: auth.Middleware now answers any request carrying a browser's Sec-Fetch-Mode (other than navigate) with plain 401 {"error":"unauthenticated"} before any provider Challenge runs — so no browser fetch() can ever receive WWW-Authenticate: Basic in ANY auth mode, and the native sign-in dialog is structurally unreachable from the app. Navigations still get each provider's challenge (branded /login redirect for local/LDAP, IdP redirect for OIDC), and WebDAV clients — which send no Sec-Fetch-* — keep the Basic challenge Finder and rclone depend on. Three tests pin all three routes, including asserting the challenge header does NOT leak to fetches.

Enforced at the one middleware rather than in each provider's Challenge for the same reason v0.5.4 fixed ErrNotFound at its definition: a rule enforced at the chokepoint covers every provider, present and future — including the Multi app-password arm that made this reachable even in OIDC mode.

Wizard handover: now lands on /setup — on a first run, creating the admin is the operator's actual next step; when first-run is not pending, /setup forwards to /login on its own, and the non-pending navigation path is popup-free by the same middleware rule.

Known degradation, accepted: pre-2023 browsers without Sec-Fetch-Mode keep the old behavior.

Shipped in **v0.5.7** (PR #169), live on both instances. **The chokepoint fix:** `auth.Middleware` now answers any request carrying a browser's `Sec-Fetch-Mode` (other than `navigate`) with plain `401 {"error":"unauthenticated"}` before any provider `Challenge` runs — so no browser `fetch()` can ever receive `WWW-Authenticate: Basic` in ANY auth mode, and the native sign-in dialog is structurally unreachable from the app. Navigations still get each provider's challenge (branded `/login` redirect for local/LDAP, IdP redirect for OIDC), and WebDAV clients — which send no `Sec-Fetch-*` — keep the Basic challenge Finder and rclone depend on. Three tests pin all three routes, including asserting the challenge header does NOT leak to fetches. Enforced at the one middleware rather than in each provider's `Challenge` for the same reason v0.5.4 fixed `ErrNotFound` at its definition: a rule enforced at the chokepoint covers every provider, present and future — including the `Multi` app-password arm that made this reachable even in OIDC mode. **Wizard handover:** now lands on `/setup` — on a first run, creating the admin is the operator's actual next step; when first-run is not pending, `/setup` forwards to `/login` on its own, and the non-pending navigation path is popup-free by the same middleware rule. Known degradation, accepted: pre-2023 browsers without `Sec-Fetch-Mode` keep the old behavior.
Cordy closed this issue 2026-08-10 00:07:34 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#168
No description provided.