Skip to content

fix: send the authorization token in a header, not the URL - #139

Merged
andre-j3sus merged 5 commits into
mainfrom
ajesus/authorization-header-token
Aug 21, 2026
Merged

fix: send the authorization token in a header, not the URL#139
andre-j3sus merged 5 commits into
mainfrom
ajesus/authorization-header-token

Conversation

@andre-j3sus

@andre-j3sus andre-j3sus commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

The optional authorizationToken was appended to request URLs as a jwt query
parameter. Request URLs are routinely persisted well outside the request itself
— server-side request logs, tracing span tags, error-reporting breadcrumbs — so
a bearer credential travelling in one leaks into every system that records a
URL.

This moves the token to an Authorization: Bearer header on the requests that
need it: the download/upload measurement requests, the load-generating
requests that run alongside them, the TURN credential fetch, and the two
logging endpoints.

Because the token is now withheld from any endpoint that isn't known to be
HTTPS, a new allowInsecureAuthorizationToken option (default false) opts
back in for local development against an http:// server. It warns once when
it takes effect.

A token alone is not enough to send one: the new authorizationEnabled option
('header' | boolean, default undefined) has to opt in. true is a permanent
alias for 'header' rather than "whatever the default transport is", so a
transport added later cannot silently change what an existing config does.

All three options are marked experimental — @experimental in the JSDoc, which
reaches the published .d.ts, and 🧪 in the README. Note authorizationToken
already shipped in v1.13.0, so this is a retroactive relabel rather than a new
option being introduced as unstable.

Request URLs are routinely persisted in server-side logs, traces and error
reports, so a bearer credential must not travel in one. The token now rides
an `Authorization: Bearer` header on the measurement, TURN credential and
logging requests.

Resolved per target URL rather than once per engine, so the existing
"never over plain HTTP" guard still holds for configs that mix schemes.

The public config is unchanged: consumers still set `authorizationToken`.
The URL-redaction helpers are gone, since no URL carries the token anymore.
@andre-j3sus andre-j3sus self-assigned this Aug 18, 2026
@ask-bonk

ask-bonk Bot commented Aug 18, 2026

Copy link
Copy Markdown
@ask-bonk

ask-bonk Bot commented Aug 18, 2026

Copy link
Copy Markdown

@andre-j3sus Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk

ask-bonk Bot commented Aug 18, 2026

Copy link
Copy Markdown
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk

ask-bonk Bot commented Aug 18, 2026

Copy link
Copy Markdown
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk

ask-bonk Bot commented Aug 18, 2026

Copy link
Copy Markdown
@andre-j3sus
andre-j3sus marked this pull request as ready for review August 18, 2026 17:33
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk

ask-bonk Bot commented Aug 18, 2026

Copy link
Copy Markdown
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk

ask-bonk Bot commented Aug 19, 2026

Copy link
Copy Markdown
@devandrepascoa

Copy link
Copy Markdown
Collaborator

Can we add a "allowInsecureAuthorizationToken: false" to allow http for local development?

The HTTPS-only guard makes the token untestable against a local `http://`
server. This adds an explicit opt-out, defaulting to false, rather than
auto-trusting loopback: the dev server is reachable over non-loopback
hostnames too, and an explicit flag is greppable in a consumer's config
where implicit detection would not be.

Warns once when it actually takes effect — a run issues hundreds of
requests, so the warning is latched rather than per-request.

The token and this flag now travel together as one `AuthorizationOptions`
through the engines, so the next transport change stays one parameter wide
instead of adding a second one to every call site.

Also fixes type errors in two test files that neither eslint (src only) nor
tsdown (src only) typechecks.
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

Added in 1ffd5b2allowInsecureAuthorizationToken, default false, warning once when it actually takes effect.

I went with the explicit flag rather than auto-trusting loopback, for two reasons. The dev server is reachable over non-loopback hostnames (allowedHosts: true), so loopback detection would silently withhold the token for LAN/tunnel setups; and an explicit option is visible and greppable in the consuming app's config, where implicit detection wouldn't be.

Two things still needed on the server side before this is testable end to end:

  1. The plaintext revocation you mentioned you're removing. Worth noting it currently fails harder than a 400 in local dev: revokeTokensSentOverHttp throws on !env.CFSPEED_API, and the top-level wrangler.jsonc has no CFSPEED_API service binding, so a token over http://localhost throws before it can return the 400.
  2. The app only fetches a token when environment === "staging", so in development authorizationToken stays null and nothing is sent regardless of what this flag allows. That gate needs to include development too.
The loaded-jitter getters return `null` with fewer than 2 samples, but the
guards only excluded `undefined`, so a `null` reached a numeric matcher and
threw `actual value must be number or bigint, received "object"`. Constrained
CI runners hit this; local runs collect enough samples and never did.

Guarding on `typeof === 'number'` can't let a null through to a numeric
matcher, and covers the `undefined` case the guards were already there for.
devandrepascoa
devandrepascoa previously approved these changes Aug 19, 2026
Addresses MR review on #139.

- `authorizationEnabled` ('header' | boolean, default undefined) opts in to
  sending the token, which is otherwise withheld even when set. `true` is
  documented as a permanent alias for 'header' rather than "the default
  transport", so adding a transport later cannot silently change what an
  existing config does. `false` is accepted so disabling can be explicit.
- Latch the insecure-transport warning per options object rather than per
  module, so a second run in the same page warns again. This needs the
  options object to be stable, so it is now built once in the constructor
  instead of per getter access. Kept out of `AuthorizationOptions` via a
  WeakSet, since that type is public and a mutable internal flag does not
  belong in it.
- Destructure `authorization` explicitly in LoggingBandwidthEngine and pass
  it back to `super`, rather than reaching into the rest spread.
- Return headers as a plain object. Note this is what surfaced the weak
  assertion in the "never puts the token in the URL" test: it only passed
  because a Headers instance serialises to `{}`, hiding the token from
  JSON.stringify rather than proving anything about the URL.
- Treat a whitespace-only token as absent, and trim padding instead of
  sending `Bearer  `.
- Omit `credentials` rather than setting it to undefined, in all three places.
- Mark the whole feature experimental: `@experimental` in the JSDoc (which
  reaches the generated .d.ts) and 🧪 in the README, with a note that these
  options may change or be removed in any release.
LoadNetworkEngine saturates the same __down/__up endpoints as a real
measurement, but its fetch was the one measurement call site not wrapped in
withAuthorizationHeader, so that traffic went out unauthenticated.

This was a regression from the header move. The old design rewrote the config
URLs — downloadApiUrl and uploadApiUrl were both in AUTHORIZABLE_URLS — so
anything consuming those URLs inherited the token for free, this engine
included. Attaching per fetch site means every site has to be enumerated, and
this one was missed.

Only reachable via packetLossUnderLoad (or packetLoss with loadDown/loadUp),
which the default measurement list does not use, so no shipped config was
affected. It is still the heaviest traffic in a test when it does run, which
is the last thing that should be silently unattributed.

Covered by a unit test that instantiates the engine, since the e2e suite skips
packet loss for CORS reasons and would never have caught it. ReachabilityEngine
remains deliberately unwrapped: the probe hosts are unrelated endpoints.

Also passes authorization to super explicitly in PacketLossUnderLoadEngine
rather than through the rest spread, matching LoggingBandwidthEngine, and
fixes two comments: an obsolete rationale on the log fetch (it referred to the
token being in the URL, which it no longer is) and a withAuthorizationHeader
docblock that had grown into design justification belonging in the PR.
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk

ask-bonk Bot commented Aug 20, 2026

Copy link
Copy Markdown
@andre-j3sus
andre-j3sus merged commit 3fbc158 into main Aug 21, 2026
6 checks passed
@andre-j3sus
andre-j3sus deleted the ajesus/authorization-header-token branch August 21, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants