Skip to content

bug(auth): cookieless anon browsers flood auth-required RPCs with 401s (~10k/3d on summarize alone) — client never detects a dead wms_ session #5207

Description

@koala73

TL;DR for the fixing agent

A cohort of real browsers runs entire sessions without the wms_ anon-session cookie and 401-floods every wms_-required RPC (~10k/3d on summarize alone, plus list-service-statuses / get-cable-health / get-chokepoint-status). The client's refresh-on-401 retries once (src/services/wm-session.ts:256-280) and then keeps hammering gated routes for the whole session — there is no dead-session circuit breaker. Separately, /api/wm-session mint outcomes are absent from telemetry, so we cannot measure how often minting fails.

Field data (Axiom wm_api_usage, 2026-07-08 → 07-11, current-browser UAs = Chrome/145–150 family)

  • /api/news/v1/summarize-article anon: 10,273 × 401 (vs 829k × 200). All 0–1 ms edge rejects, reason: auth_401.
  • Same-client route split (sampled per CF edge IP): token-optional routes 200 (record-baseline-snapshot ~1.7k, get-humanitarian-summary-batch ~1k, get-aircraft-details-batch ~700, get-fred-series-batch) while wms_-required routes 401 forever (list-service-statuses 341–372, get-cable-health ~180, get-chokepoint-status ~160, summarize-article 70–96). The app runs fine; only the session cookie is missing → classic 4xx-flood pattern (cf. fix(summarization): anon dashboards dispatch premium summarize-article RPCs — 401 flood + 3-provider fan-out since #4687 (client caller never swept) #4913's motivation).
  • Digest (list-feed-digest) current-browser 401s are comparatively rare (1,634 vs 41k 200s) — the 200-heavy digest path is largely same-origin (origin_kind: null), consistent with third-party-cookie loss on the cross-site api host being a driver.

⚠️ Measurement caveats

  1. ip is often the Cloudflare edge IP (172.70.x/162.158.x/104.23.x) — do NOT size the cohort with dcount(ip); route-level status mixes are the valid signal.
  2. Bot traffic with faked UAs inflates raw counts (confirmed on digest: Bun/1.3.13 from 6 IPs, frozen Chrome/134 from 7 IPs, EliteGold-HYPE-Worker/1.0, bare Mozilla/5.0 — all correctly rejected; NOT this issue).
  3. Historical: 2026-07-04/05 abuse wave (833k anon 401s on 07-05 from 1,388 IPs, self-resolved 07-06) poisons any 7-day aggregate — window queries after 07-06.

Code map (worktree @ 3ccfe21)

  • Cookie mint: api/wm-session.js (tests: api/wm-session.test.mjs). HttpOnly wms_ cookie; JS tracks only expiry (12 h, 30-min periodic refresh).
  • Client interceptor: src/services/wm-session.ts:174 installWmSessionFetchInterceptor() — patches global fetch, adds credentials for API-origin calls.
  • Retry-once: wm-session.ts:256-280 — on 401, invalidate cached expiry, re-mint, retry; if the retry also 401s it warns once (retryRejectedWarned, :276) and returns the 401 — every subsequent gated call repeats the full mint+retry cycle. The file's own comments document two prior total-401 regressions (matcher under-match, 2026-05-03; premium-path rejection).
  • Server validation: api/_api-key.js validateApiKey — anon browser access requires the wms_ token; Origin/Referer/Sec-Fetch are deliberately not trusted ([SECURITY P0] API key bypass via Referer header — _api-key.js trusts client-controlled Referer when Origin is absent #3541).
  • Telemetry classifier: server/_shared/usage.ts:429-444 deriveOriginKind — Bearer ⇒ 'oauth' takes precedence, no-Origin ⇒ null; useful when cohorting.

Hypotheses to discriminate (in order)

  1. Third-party cookie blocking: www.worldmonitor.app → api.worldmonitor.app is cross-site; wms_ is third-party there. Chrome incognito default / "block third-party cookies" / Brave never send it. Discriminator: the cookie's attributes in api/wm-session.js (needs SameSite=None; Secure; is Partitioned/CHIPS set?) + a local repro (below).
  2. Mint failure/race: unmeasurable today — /api/wm-session does not emit into wm_api_usage (verified: zero rows). Add telemetry as part of the fix.
  3. Residual bots faking current-Chrome UAs (do not let them dominate the verify metric; scope to origin_kind in ('browser-cross-origin','browser-same-origin')).

Repro (do this first — Bug Fix Protocol)

  1. Chrome → Settings → block third-party cookies (or use incognito) → load https://www.worldmonitor.app → console shows the 401 flood on gated RPCs while token-optional panels populate; observe the app KEEPS firing gated calls every refresh cycle.
  2. cURL sanity check (no cookie): curl -s -o /dev/null -w '%{http_code}' -X POST 'https://api.worldmonitor.app/api/news/v1/summarize-article' -H 'content-type: application/json' -H 'origin: https://www.worldmonitor.app' -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36' -d '{}' → 401.

Fix directions

  1. Client circuit-breaker (highest value): when the mint+retry cycle fails (the wm-session.ts:276 site), set a session-dead flag consumed by the interceptor/RPC callers — render the anon-degraded state once and stop auto-firing wms_-gated RPCs for a cool-down. Kills the flood AND the wasted mint round-trips.
  2. Mint telemetry: emit /api/wm-session outcomes into wm_api_usage so mint success rate is a queryable number.
  3. Cookie survivability: evaluate Partitioned (CHIPS) on the wms_ Set-Cookie in api/wm-session.js, or route wms_-gated RPCs same-origin on www (the digest's same-origin path is the working counter-example). Note SW/CDN implications before switching origins.

Acceptance

  • Local repro: cookie-blocked session fires ≤ N (e.g. 3) gated RPC attempts then stops; UI shows degraded state instead of silent empty panels.
  • APL (post-deploy, browser-origin only): ['wm_api_usage'] | where status==401 and reason=='auth_401' and origin_kind startswith 'browser' | summarize n=count() by bin(_time,1d) → sustained drop.
  • New mint telemetry visible: ['wm_api_usage'] | where route contains 'wm-session' | summarize count() by status.

Found via DebugBear synthetic run (fresh profile → [Summarization] All providers failed ×5) → field triage 2026-07-11. Related: #3541 (wms_ design), #4913 (client entitlement gate — same flood-prevention philosophy), sibling issue #5206 (the 429 asymmetry on the same route).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions