diff --git a/.changeset/proxy-no-duplicate-auth.md b/.changeset/proxy-no-duplicate-auth.md new file mode 100644 index 0000000..88f5bf3 --- /dev/null +++ b/.changeset/proxy-no-duplicate-auth.md @@ -0,0 +1,9 @@ +--- +'@interline-io/tlv2-auth': patch +--- + +Fix authenticated proxy requests being rejected by the backend with `token is malformed: token contains an invalid number of segments` (HTTP 401). + +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 , Bearer "`. Switched 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's irrelevant to the API and the encrypted auth0-nuxt session cookie duplicates the JWT. diff --git a/src/runtime/plugins/auth.server.ts b/src/runtime/plugins/auth.server.ts index 8354de5..04d646f 100644 --- a/src/runtime/plugins/auth.server.ts +++ b/src/runtime/plugins/auth.server.ts @@ -67,8 +67,12 @@ const plugin: Plugin = defineNuxtPlugin((nuxtApp) => { if (!isBackendRequest(effectiveUrl)) { return } 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 , Bearer ", 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) } options.headers = headers } @@ -84,8 +88,12 @@ const plugin: Plugin = defineNuxtPlugin((nuxtApp) => { 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 , Bearer ", which the backend rejects as a malformed JWT. for (const [key, value] of Object.entries(authHeaders)) { - headers.append(key, value) + headers.set(key, value) } init.headers = headers return originalFetch(input, init) diff --git a/src/runtime/util/proxy.ts b/src/runtime/util/proxy.ts index b70b6cb..f78a666 100644 --- a/src/runtime/util/proxy.ts +++ b/src/runtime/util/proxy.ts @@ -25,6 +25,12 @@ export async function proxyHandler ( const query = getQuery(event) const requestApikey = (query.apikey ? query.apikey.toString() : '') || event.headers.get('apikey') || '' const headers = buildProxyHeaders(graphqlApikey, accessToken, requestApikey) + // Never forward the browser session cookie to the backend API. It's + // irrelevant to the API, and the encrypted auth0-nuxt session cookie + // effectively duplicates the JWT we already attach. h3's mergeHeaders treats + // an empty string as an override (undefined would be ignored), so this + // replaces the forwarded Cookie rather than leaving it intact. + headers.cookie = '' const target = buildProxyTarget(proxyBase, pathOverride ?? event.path) if (traceEnabled) {