🐛 Forward Strava _strava_idcf cookie and retry on 401 #8

Merged
mat merged 2 commits from fix/forward-strava-idcf into main 2026-10-02 12:30:41 +00:00
Owner

Summary

 CookieManager (proxy/src/cookies.rs)
   parse_set_cookies
+    "_strava_idcf" => idcf = Some(value)        // same parse style as the trio
   cookie_header()
-    "CloudFront-Key-Pair-Id=…; CloudFront-Policy=…; CloudFront-Signature=…"
+    … ; _strava_idcf=<jwt>                      // appended only when present

 get_tile (proxy/src/app.rs)
-  refresh+refetch on upstream 403
+  refresh+refetch on upstream 403 or 401

Strava's identified tile endpoint now also requires the _strava_idcf JWT (short-lived, athlete-bound) alongside the CloudFront trio — trio-only requests get 401. That JWT is emitted as a Set-Cookie by the very exchange response fetch_cookies already parses; we were dropping it. This captures and forwards it, and teaches the cookie-recovery path the 401 signature (the 403-only trigger couldn't recover an idcf that expires or is invalidated inside the 12 h refresh window).

Evidence

  • Before (live production probes, 2026-10-02): exchange yields a valid identified-scope trio (same policy + key-pair as a working browser session), yet content-a.strava.com/identified/… returns 401 for trio-only and 200 PNG for trio + _strava_idcf → every zoom 12–15 request through canicule was a 502.
  • After: cargo test -p canicule — 44 passed (9 suites), incl. new:
    • forwards_the_strava_idcf_cookie_on_authenticated_tile_requests — exact wire-level Cookie-header assertion (red against pre-fix code)
    • reauthenticates_after_an_upstream_401_when_the_idcf_cookie_is_missing — mock tile endpoint returns 401 unless the Cookie header carries _strava_idcf=; asserts final 200 through refresh+refetch (red was 502 vs 200, the production shape)
    • unit: idcf captured with cookie attributes; absent idcf → byte-identical trio-only header, no trailing "; "
  • cargo fmt --check and clippy --all-targets clean.

Merge Danger

Door: two-way — revert restores current (hi-res-broken) behaviour; anonymous zoom ≤ 11 unaffected either way.

Blast Radius: low — authenticated tile path only. Absent-idcf behaviour is byte-identical to today's, pinned by tests. No leak path: the idcf value only flows into the upstream request; visitor responses carry tile bytes only. Post-deploy check: one zoom 12–15 tile through the proxy after the next tofu-maison apply (Renovate bumps the pinned ref automatically).

Related: findings also posted on #6 — the login/password flow must maintain _strava4_session and _strava_idcf.

## Summary ```diff CookieManager (proxy/src/cookies.rs) parse_set_cookies + "_strava_idcf" => idcf = Some(value) // same parse style as the trio cookie_header() - "CloudFront-Key-Pair-Id=…; CloudFront-Policy=…; CloudFront-Signature=…" + … ; _strava_idcf=<jwt> // appended only when present get_tile (proxy/src/app.rs) - refresh+refetch on upstream 403 + refresh+refetch on upstream 403 or 401 ``` Strava's identified tile endpoint now also requires the `_strava_idcf` JWT (short-lived, athlete-bound) alongside the CloudFront trio — trio-only requests get **401**. That JWT is emitted as a `Set-Cookie` by the very exchange response `fetch_cookies` already parses; we were dropping it. This captures and forwards it, and teaches the cookie-recovery path the 401 signature (the 403-only trigger couldn't recover an idcf that expires or is invalidated inside the 12 h refresh window). ## Evidence - **Before** (live production probes, 2026-10-02): exchange yields a valid identified-scope trio (same policy + key-pair as a working browser session), yet `content-a.strava.com/identified/…` returns **401** for trio-only and **200 PNG** for trio + `_strava_idcf` → every zoom 12–15 request through canicule was a 502. - **After:** `cargo test -p canicule` — **44 passed (9 suites)**, incl. new: - `forwards_the_strava_idcf_cookie_on_authenticated_tile_requests` — exact wire-level Cookie-header assertion (red against pre-fix code) - `reauthenticates_after_an_upstream_401_when_the_idcf_cookie_is_missing` — mock tile endpoint returns 401 unless the Cookie header carries `_strava_idcf=`; asserts final 200 through refresh+refetch (red was 502 vs 200, the production shape) - unit: idcf captured with cookie attributes; absent idcf → byte-identical trio-only header, no trailing `"; "` - `cargo fmt --check` and `clippy --all-targets` clean. ## Merge Danger **Door:** two-way — revert restores current (hi-res-broken) behaviour; anonymous zoom ≤ 11 unaffected either way. **Blast Radius:** low — authenticated tile path only. Absent-idcf behaviour is byte-identical to today's, pinned by tests. No leak path: the idcf value only flows into the upstream request; visitor responses carry tile bytes only. Post-deploy check: one zoom 12–15 tile through the proxy after the next tofu-maison apply (Renovate bumps the pinned ref automatically). Related: findings also posted on #6 — the login/password flow must maintain `_strava4_session` **and** `_strava_idcf`.
🐛 Retry cookie refresh on upstream 401 too
All checks were successful
CI / rust (pull_request) Successful in 1m37s
CI / frontend (pull_request) Successful in 27s
CI / e2e (pull_request) Successful in 30s
6eb84cecd0
mat changed title from WIP: 🐛 Forward Strava _strava_idcf cookie and retry on 401 to 🐛 Forward Strava _strava_idcf cookie and retry on 401 2026-10-02 12:30:38 +00:00
mat merged commit 72dd3741d6 into main 2026-10-02 12:30:41 +00:00
mat deleted branch fix/forward-strava-idcf 2026-10-02 12:30:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
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
mat/canicule!8
No description provided.