[GH-ISSUE #8858] OAuth provider: HMAC signature verification fails when CDN/proxy reorders query parameters #19847

Open
opened 2026-04-15 19:11:59 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @Lyeed on GitHub (Mar 31, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8858

Originally assigned to: @gustavovalverde on GitHub.

Description

The OAuth provider plugin's signParams and verifyOAuthQueryParams functions compute HMAC signatures over URLSearchParams.toString(), which preserves insertion order. If any intermediary (CDN, reverse proxy, edge layer) reorders URL query parameters in 302 redirect Location headers, the signature verification fails with invalid_signature.

This is a real-world issue on Vercel (their edge layer reorders query params in redirects — see vercel/next.js#39017), but could affect any deployment behind a CDN that normalizes URLs.

Steps to reproduce

  1. Deploy a SvelteKit app with @better-auth/oauth-provider on Vercel with Deployment Protection enabled
  2. Start an OAuth authorization flow (/api/auth/oauth2/authorize)
  3. The authorize endpoint signs query params and redirects to /signin?...&exp=X&sig=Y
  4. Vercel's edge layer reorders the query params in the Location header
  5. The browser receives the URL with params in a different order
  6. On sign-in, oauthProviderClient sends the reordered params as oauth_query
  7. verifyOAuthQueryParams computes HMAC over the reordered string → mismatchinvalid_signature

Additionally, the client-side parseSignedQuery iterates params and stops at sig, assuming it's the last parameter. If sig gets reordered to the middle, the query is truncated.

Root cause

In packages/oauth-provider/src/authorize.ts (signParams):

const params = new URLSearchParams(ctx.query);
params.set("exp", String(exp));
const signature = await makeSignature(params.toString(), ctx.context.secret);

In packages/oauth-provider/src/utils/index.ts (verifyOAuthQueryParams):

queryParams.delete("sig");
const verifySig = await makeSignature(queryParams.toString(), secret);

URLSearchParams.toString() preserves insertion order. URLs with the same parameters in different orders are semantically equivalent per RFC 3986, so HMAC signatures should be order-independent.

Proposed fix

Call URLSearchParams.sort() before computing the HMAC in both signParams and verifyOAuthQueryParams. This ensures deterministic parameter ordering regardless of how they arrive. This is the same approach used by AWS Signature V4.

// In signParams:
  const params = new URLSearchParams(ctx.query);
  params.set("exp", String(exp));
+ params.sort();
  const signature = await makeSignature(params.toString(), ctx.context.secret);

// In verifyOAuthQueryParams:
  queryParams.delete("sig");
+ queryParams.sort();
  const verifySig = await makeSignature(queryParams.toString(), secret);

This is a non-breaking change — existing signatures would need to be regenerated on the next authorization flow (they expire in 10 minutes anyway via the exp field).

Environment

  • @better-auth/oauth-provider@1.5.6
  • SvelteKit on Vercel (serverless)
  • Vercel Deployment Protection enabled
Originally created by @Lyeed on GitHub (Mar 31, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8858 Originally assigned to: @gustavovalverde on GitHub. ## Description The OAuth provider plugin's `signParams` and `verifyOAuthQueryParams` functions compute HMAC signatures over `URLSearchParams.toString()`, which preserves insertion order. If any intermediary (CDN, reverse proxy, edge layer) reorders URL query parameters in 302 redirect `Location` headers, the signature verification fails with `invalid_signature`. This is a real-world issue on **Vercel** (their edge layer reorders query params in redirects — see [vercel/next.js#39017](https://github.com/vercel/next.js/issues/39017)), but could affect any deployment behind a CDN that normalizes URLs. ## Steps to reproduce 1. Deploy a SvelteKit app with `@better-auth/oauth-provider` on Vercel with Deployment Protection enabled 2. Start an OAuth authorization flow (`/api/auth/oauth2/authorize`) 3. The authorize endpoint signs query params and redirects to `/signin?...&exp=X&sig=Y` 4. Vercel's edge layer reorders the query params in the `Location` header 5. The browser receives the URL with params in a different order 6. On sign-in, `oauthProviderClient` sends the reordered params as `oauth_query` 7. `verifyOAuthQueryParams` computes HMAC over the reordered string → **mismatch** → `invalid_signature` Additionally, the client-side `parseSignedQuery` iterates params and stops at `sig`, assuming it's the last parameter. If `sig` gets reordered to the middle, the query is truncated. ## Root cause In `packages/oauth-provider/src/authorize.ts` (`signParams`): ```ts const params = new URLSearchParams(ctx.query); params.set("exp", String(exp)); const signature = await makeSignature(params.toString(), ctx.context.secret); ``` In `packages/oauth-provider/src/utils/index.ts` (`verifyOAuthQueryParams`): ```ts queryParams.delete("sig"); const verifySig = await makeSignature(queryParams.toString(), secret); ``` `URLSearchParams.toString()` preserves insertion order. URLs with the same parameters in different orders are semantically equivalent per RFC 3986, so HMAC signatures should be order-independent. ## Proposed fix Call `URLSearchParams.sort()` before computing the HMAC in both `signParams` and `verifyOAuthQueryParams`. This ensures deterministic parameter ordering regardless of how they arrive. This is the same approach used by AWS Signature V4. ```diff // In signParams: const params = new URLSearchParams(ctx.query); params.set("exp", String(exp)); + params.sort(); const signature = await makeSignature(params.toString(), ctx.context.secret); // In verifyOAuthQueryParams: queryParams.delete("sig"); + queryParams.sort(); const verifySig = await makeSignature(queryParams.toString(), secret); ``` This is a non-breaking change — existing signatures would need to be regenerated on the next authorization flow (they expire in 10 minutes anyway via the `exp` field). ## Environment - `@better-auth/oauth-provider@1.5.6` - SvelteKit on Vercel (serverless) - Vercel Deployment Protection enabled
GiteaMirror added the oauthbug labels 2026-04-15 19:11:59 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19847