From 1b8b85f685ee6b2812746d55730d8bf7c1b20d0c Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 23 Feb 2026 13:23:27 +0000 Subject: [PATCH] fix: use opaque state parameter in OAuth authorization flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace base64-encoded AuthRequest in the OAuth state parameter with an opaque UUID token. The full AuthRequest is already stored in KV via createOAuthState() — embedding it in the URL was redundant and caused authorization URLs to exceed Cloudflare's size limits when combined with CIMD client IDs or many scopes. Security is unchanged: state is still validated via KV lookup + SHA-256 session cookie binding + single-use deletion. --- src/auth/cloudflare-auth.ts | 6 ++---- src/auth/oauth-handler.ts | 22 ++++------------------ src/auth/workers-oauth-utils.ts | 12 +----------- 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/src/auth/cloudflare-auth.ts b/src/auth/cloudflare-auth.ts index 0ed18d9..2b38859 100644 --- a/src/auth/cloudflare-auth.ts +++ b/src/auth/cloudflare-auth.ts @@ -1,7 +1,5 @@ import { z } from 'zod' -import type { AuthRequest } from '@cloudflare/workers-oauth-provider' - const PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' const CODE_VERIFIER_LENGTH = 96 @@ -44,7 +42,7 @@ export async function generatePKCECodes(): Promise { export async function getAuthorizationURL(params: { client_id: string redirect_uri: string - state: AuthRequest + stateToken: string scopes: string[] codeChallenge: string }): Promise<{ authUrl: string }> { @@ -52,7 +50,7 @@ export async function getAuthorizationURL(params: { response_type: 'code', client_id: params.client_id, redirect_uri: params.redirect_uri, - state: btoa(JSON.stringify(params.state)), + state: params.stateToken, code_challenge: params.codeChallenge, code_challenge_method: 'S256', scope: params.scopes.join(' ') diff --git a/src/auth/oauth-handler.ts b/src/auth/oauth-handler.ts index 1718506..e98950c 100644 --- a/src/auth/oauth-handler.ts +++ b/src/auth/oauth-handler.ts @@ -135,21 +135,15 @@ export async function handleTokenExchangeCallback( */ async function redirectToCloudflare( requestUrl: string, - oauthReqInfo: AuthRequest, stateToken: string, codeChallenge: string, scopes: string[], additionalHeaders: Record = {} ): Promise { - const stateWithToken: AuthRequest = { - ...oauthReqInfo, - state: stateToken - } - const { authUrl } = await getAuthorizationURL({ client_id: env.CLOUDFLARE_CLIENT_ID, redirect_uri: new URL('/oauth/callback', requestUrl).href, - state: stateWithToken, + stateToken, scopes, codeChallenge }) @@ -193,16 +187,9 @@ export function createAuthHandlers() { const stateToken = await createOAuthState(oauthReqInfo, env.OAUTH_KV, codeVerifier) const { setCookie: sessionCookie } = await bindStateToSession(stateToken) - return redirectToCloudflare( - c.req.url, - oauthReqInfo, - stateToken, - codeChallenge, - defaultScopes, - { - 'Set-Cookie': sessionCookie - } - ) + return redirectToCloudflare(c.req.url, stateToken, codeChallenge, defaultScopes, { + 'Set-Cookie': sessionCookie + }) } // Client not approved - show consent dialog with scope selection @@ -263,7 +250,6 @@ export function createAuthHandlers() { const redirectResponse = await redirectToCloudflare( c.req.url, - oauthReqInfo, stateToken, codeChallenge, scopesToRequest diff --git a/src/auth/workers-oauth-utils.ts b/src/auth/workers-oauth-utils.ts index c6ff380..c771eab 100644 --- a/src/auth/workers-oauth-utils.ts +++ b/src/auth/workers-oauth-utils.ts @@ -1101,17 +1101,7 @@ export async function validateOAuthState( throw new OAuthError('invalid_request', 'Missing state parameter') } - // Decode state to extract embedded stateToken - let stateToken: string - try { - const decodedState = JSON.parse(atob(stateFromQuery)) - stateToken = decodedState.state - if (!stateToken) { - throw new Error('State token not found') - } - } catch { - throw new OAuthError('invalid_request', 'Failed to decode state') - } + const stateToken = stateFromQuery // Validate state exists in KV const storedDataJson = await kv.get(`oauth:state:${stateToken}`)