Skip to content

fix(security): RQ-3841 read the auth token from the URL fragment - #114

Open
rohitneharabrowserstack wants to merge 1 commit into
masterfrom
rq-3841-read-token-from-fragment
Open

fix(security): RQ-3841 read the auth token from the URL fragment#114
rohitneharabrowserstack wants to merge 1 commit into
masterfrom
rq-3841-read-token-from-fragment

Conversation

@rohitneharabrowserstack

@rohitneharabrowserstack rohitneharabrowserstack commented Aug 25, 2026

Copy link
Copy Markdown

Client half of RQ-3841. Merge and deploy this before the requestly-cloud half.

Why

/oauth/callback currently redirects to app.requestly.io/?accessToken=<firebase custom token>. A token in the query string is recorded in:

  • browser history
  • the Referer header sent to every third-party script this page loads (Amplitude, Sentry, GrowthBook)
  • GCP / Firebase HTTP access logs, and any CDN or proxy in between

The token exchanges for a Firebase refresh token, so one leaked log line becomes durable account access. Moving it to the fragment fixes this: browsers never send a fragment to a server.

Ordering — this must land first

The server change switches the token to the fragment. A client that only reads window.location.search would then find no token and every OAuth login would break.

This PR makes the client accept either location, so the two repos can deploy in any order:

new URLSearchParams(hash).get("accessToken") ?? params.get("accessToken")

The query-string fallback also covers cached / in-flight callback URLs. It can be deleted once the server change is live in beta and production.

Changes

  • Read the fragment first, fall back to the query string. Captured in a useMemo on mount, so clearing the URL later cannot null it out mid-flow.
  • clearAccessTokenFromUrl() — once the token is captured, rewrite the address bar with history.replaceState (no new history entry) so the credential is not left visible to be copied, bookmarked, or read by an extension. Strips the whole fragment plus any legacy query accessToken, while preserving every other param the flow reads (isNewUser, redirectURL).

Verification

  • Parses cleants.transpileModule, 0 syntax diagnostics. I could not run a full tsc/lint: I worked from a sparse shallow clone without the app's dependencies installed, so please let CI be the real check here.
  • Fragment round-trip tested with a token containing +, / and =. URLSearchParams percent-encodes on write and decodes on read; naive string concatenation would corrupt such a token.
  • Desktop flow unaffectedgetDesktopAppAuthParams() reads localStorage, not the URL, so clearing the address bar cannot disturb auth_mode / ot-auth-code.

Reviewer notes

  1. The useEffect dependency array gained accessToken. Behaviour is unchanged in practice (params is []-memoized, so accessToken is stable for the component's life), but worth a look.
  2. I'd like confirmation that no other surface reads ?accessToken from this redirect. I found only this component plus AuthPage.js / FirebaseActions.js / AuthHandler.ts referencing signInWithCustomToken, and none of the others parse the URL — but I was searching a sparse checkout.

🤖 Generated with Claude Code

Prepares the client for requestly-cloud moving the Firebase custom token out of
the query string. Ship and deploy this BEFORE the server change: the server
half switches the token to the fragment, and a client that only reads
window.location.search would break every OAuth login.

- accessToken is now read from the fragment first, falling back to the query
  string. The fallback keeps this deploy-order-independent and covers cached or
  in-flight callback URLs; it can be dropped once the server change is live
  everywhere.
- clearAccessTokenFromUrl() rewrites the address bar via replaceState once the
  token is captured, so the credential is not left visible to be copied,
  bookmarked, or read by a browser extension. It strips the whole fragment plus
  a legacy query accessToken, preserving every other param the flow reads
  (isNewUser, redirectURL).
- The token is captured in a useMemo on mount, so clearing the URL afterwards
  cannot null it out mid-flow.

Why the fragment: browsers never send it to a server, so the token stays out of
Referer headers (this page loads Amplitude / Sentry / GrowthBook), out of GCP
and CDN access logs, and out of anything else that records request URLs.

Verified: file parses clean (tsc transpile, 0 syntax diagnostics); fragment
round-trip checked with a token containing '+', '/' and '=' -- URLSearchParams
percent-encodes on write and decodes on read, which naive concatenation would
corrupt. getDesktopAppAuthParams reads localStorage, not the URL, so the
desktop auth flow is unaffected by clearing the address bar.

Server half: requestly-cloud RQ-3841.
Refs RQ-3841

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant