Fix duplicate auth headers and cookie forwarding in proxy#23
Open
irees wants to merge 2 commits into
Open
Conversation
The auth.server plugin injected Authorization/apikey onto backend requests
with Headers.append(). Because the server-side proxy already attaches those
headers, the request — which flows through the patched globalThis.fetch — had
them appended a second time, comma-joining into
"Authorization: Bearer <tok>, Bearer <tok>". The backend parsed that as a
malformed JWT and rejected it with 401 ("token contains an invalid number of
segments"), breaking every authenticated GraphQL/REST call and the `me`
role-enrichment query. Switch both injection paths ($fetch onRequest and the
globalThis.fetch override) to Headers.set(), which is idempotent.
Also stop forwarding the browser session cookie to the backend API in the
proxy: it is irrelevant to the API and the encrypted auth0-nuxt session cookie
duplicates the JWT. Uses an empty-string override since h3's mergeHeaders
ignores undefined but applies empty strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first commit only converted the $fetch onRequest loop; the two injection loops have different indentation so the bulk replace missed the globalThis.fetch override — which is the exact path the proxy's outbound request flows through. Convert it too so authenticated proxy requests don't get a duplicated Authorization header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes an SSR/server-proxy authentication failure caused by duplicated Authorization/apikey headers being appended twice, and prevents forwarding the browser’s Auth0 session cookie upstream to the backend API.
Changes:
- Switch server-side auth header injection in
auth.serverfromHeaders.append()toHeaders.set()to avoid comma-joined duplicate auth headers. - Clear the
cookieheader in the proxy handler to prevent forwarding the browser session cookie to backend APIs. - Add a changeset documenting the patch-level fix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/runtime/util/proxy.ts |
Clears cookie before proxying to prevent forwarding browser session cookies upstream. |
src/runtime/plugins/auth.server.ts |
Uses Headers.set() to avoid duplicate Authorization/apikey headers in SSR/server requests. |
.changeset/proxy-no-duplicate-auth.md |
Records the patch release notes for the proxy/auth header fixes. |
Comments suppressed due to low confidence (1)
src/runtime/plugins/auth.server.ts:97
- Same issue in the
globalThis.fetchwrapper:headers.set()will override an existingapikeyheader (including user-supplied keys forwarded by the server proxy). To preserve the documented precedence of caller-provided keys, skip injectingapikeywhen the request already has one (or when it differs), and only inject the default when missing.
const authHeaders = await getAuthHeaders()
init = init || {}
const headers = new Headers(init.headers || {})
// set() not append(): the request may already carry Authorization/apikey
// (the server-side proxy pre-authenticates its outbound request, which
// flows through this override). append() would comma-join into
// "Bearer <tok>, Bearer <tok>", which the backend rejects as a malformed JWT.
for (const [key, value] of Object.entries(authHeaders)) {
headers.set(key, value)
}
Comment on lines
68
to
76
| const authHeaders = await getAuthHeaders() | ||
| const headers = new Headers(options.headers || {}) | ||
| // set() not append(): a request may already carry Authorization/apikey | ||
| // (e.g. the server-side proxy pre-authenticates its outbound request). | ||
| // append() would comma-join into "Bearer <tok>, Bearer <tok>", which the | ||
| // backend parses as a malformed JWT and rejects with 401. | ||
| for (const [key, value] of Object.entries(authHeaders)) { | ||
| headers.append(key, value) | ||
| headers.set(key, value) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes authenticated proxy requests being rejected by the backend API with
token is malformed: token contains an invalid number of segments(HTTP 401). Two related bugs in the server-side auth/proxy path, found while migratingwww-transit-land-v2offtlv2-ui.Duplicate
Authorizationheader (the 401)auth.serveroverridesglobalThis.$fetchandglobalThis.fetchto injectAuthorization/apikeyon requests to configured backend origins, usingHeaders.append(). The server-side proxy (buildProxyHeaders) already attaches those headers, and its outbound request flows through the patchedglobalThis.fetch— so the headers were appended a second time, comma-joining into:The backend strips the first
Bearerprefix, parses<tok>, Bearer <tok>, gets the wrong number of dot-separated segments, and rejects the token. This broke every authenticated GraphQL/REST call through the proxy and themerole-enrichment query (empty roles → role-gated UI shows "Feature unavailable").Fix: both injection paths now use
Headers.set()instead ofappend().set()is idempotent — when a request already carries the header (the proxy case), it collapses to a single correct value rather than duplicating.Browser session cookie forwarded upstream
The proxy forwarded the incoming browser headers wholesale (h3's default), including the
__a0_sessioncookie. That cookie is irrelevant to the backend API and — being the encrypted auth0-nuxt session — effectively duplicates the JWT we already attach, leaking it to the upstream.Fix: set
headers.cookie = ''beforeproxyRequest. h3'smergeHeadersignoresundefinedbut applies empty strings, so this overrides the forwardedCookierather than leaving it intact.Test plan
pnpm lint✓pnpm test✓ (35/35)www-transit-land-v2's proxy: authenticated GraphQL requests return data (previously 401), and the wire request to the backend carries exactly oneAuthorizationand no session cookie.🤖 Generated with Claude Code