From 02009f098fb6ebe70668c92016179904056ce20b Mon Sep 17 00:00:00 2001 From: saltbo Date: Sun, 2 Aug 2026 20:27:31 -0400 Subject: [PATCH] fix(auth): normalize Workers preview requests --- server/auth.integration.test.ts | 22 +++++++++++++++++++++- server/auth.ts | 17 ++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/server/auth.integration.test.ts b/server/auth.integration.test.ts index 75efe307..11b9c6e4 100644 --- a/server/auth.integration.test.ts +++ b/server/auth.integration.test.ts @@ -11,7 +11,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { createInviteRepo } from './adapters/repos/invite.js' import { createSiteInvitationRepo } from './adapters/repos/site-invitations.js' import { createApp } from './app.js' -import { createAuth } from './auth.js' +import { canonicalAuthRequest, createAuth } from './auth.js' import * as authSchema from './db/auth-schema.js' import * as schema from './db/schema.js' import { inviteCodes, siteInvitations } from './db/schema.js' @@ -759,6 +759,26 @@ describe('Cloudflare Workers preview auth origins', () => { const commitOrigin = 'https://99dc50ae-zpan.saltbo.workers.dev' const branchOrigin = 'https://feat-x402-paid-agent-uploads-zpan.saltbo.workers.dev' + it('normalizes official preview requests to the configured authorization server URL', async () => { + const request = new Request(`${branchOrigin}/api/auth/get-session`, { + headers: { Origin: branchOrigin, 'X-Request-ID': 'preview-request' }, + }) + const normalized = canonicalAuthRequest(request, configuredOrigin) + + expect(normalized.url).toBe(`${configuredOrigin}/api/auth/get-session`) + expect(normalized.headers.get('origin')).toBe(branchOrigin) + expect(normalized.headers.get('x-request-id')).toBe('preview-request') + const canonicalRequest = new Request(`${configuredOrigin}/api/auth/get-session`) + expect(canonicalAuthRequest(canonicalRequest, configuredOrigin)).toBe(canonicalRequest) + + const post = canonicalAuthRequest( + new Request(`${commitOrigin}/api/auth/oauth2/token`, { method: 'POST', body: 'grant_type=authorization_code' }), + configuredOrigin, + ) + expect(post.method).toBe('POST') + expect(await post.text()).toBe('grant_type=authorization_code') + }) + it('accepts official commit and branch aliases on the same cached auth instance', async () => { const ctx = await createTestApp() const auth = await createAuth(ctx.platform, 'test-secret', configuredOrigin, [configuredOrigin]) diff --git a/server/auth.ts b/server/auth.ts index 79d45182..391fa88d 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -173,6 +173,16 @@ export function officialWorkersPreviewOrigin( } } +export function canonicalAuthRequest(request: Request, baseURL: string | undefined): Request { + const requestUrl = new URL(request.url) + if (!officialWorkersPreviewOrigin(baseURL, requestUrl.origin)) return request + + const canonicalUrl = new URL(baseURL!) + requestUrl.protocol = canonicalUrl.protocol + requestUrl.host = canonicalUrl.host + return new Request(requestUrl, request) +} + // One query loads every oauth_provider_* row. Configs are snapshotted at auth // instance creation: better-auth resolves social providers eagerly during its // context init, so per-request dynamic loading is not possible anyway. Admin @@ -951,15 +961,16 @@ export async function createAuth( const defaultAuth = await createAuthInstance(false) let verificationAuth: typeof defaultAuth | null = null const dynamicHandler = async (request: Request): Promise => { - if (!usesEmailVerificationPolicy(request)) return defaultAuth.handler(request) + const authRequest = canonicalAuthRequest(request, baseURL) + if (!usesEmailVerificationPolicy(authRequest)) return defaultAuth.handler(authRequest) const required = isEmailVerificationRequired( await systemOptionsRepo.getValue(EMAIL_VERIFICATION_REQUIRED_OPTION_KEY), ) - if (!required) return defaultAuth.handler(request) + if (!required) return defaultAuth.handler(authRequest) verificationAuth ??= await createAuthInstance(true) - return verificationAuth.handler(request) + return verificationAuth.handler(authRequest) } return new Proxy(defaultAuth, {