From 43ebe7ed138aebfcbd81b34432240fce48847891 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 9 Apr 2026 11:02:10 +0200 Subject: [PATCH 1/2] fix: redirect error --- src/betterAuth.ts | 15 +++++++++++++++ src/routes/redirects.ts | 26 +++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/betterAuth.ts b/src/betterAuth.ts index 5012c542ac..00214dee51 100644 --- a/src/betterAuth.ts +++ b/src/betterAuth.ts @@ -287,17 +287,32 @@ const cookieDomain = process.env.BETTER_AUTH_BASE_URL ? extractRootDomain(new URL(process.env.BETTER_AUTH_BASE_URL).hostname) : undefined; +const getOAuthErrorRedirectUrl = (): string | undefined => { + const webappUrl = process.env.COMMENTS_PREFIX; + if (!webappUrl) { + return undefined; + } + return `${webappUrl}/callback`; +}; + export const getBetterAuthOptions = (pool: Pool): BetterAuthOptions => { const trustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS ? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(',') : []; + const oauthErrorRedirectUrl = getOAuthErrorRedirectUrl(); + return { database: pool, baseURL: process.env.BETTER_AUTH_BASE_URL || 'http://localhost:3000', basePath: '/auth', secret: process.env.BETTER_AUTH_SECRET ?? '', trustedOrigins, + ...(oauthErrorRedirectUrl && { + onAPIError: { + errorURL: oauthErrorRedirectUrl, + }, + }), secondaryStorage: { get: (key) => singleRedisClient.get(`ba:${key}`), set: (key, value, ttl) => diff --git a/src/routes/redirects.ts b/src/routes/redirects.ts index 7687dcfb30..528891ee59 100644 --- a/src/routes/redirects.ts +++ b/src/routes/redirects.ts @@ -171,9 +171,29 @@ const redirectToProfileImage = async ( export default async function (fastify: FastifyInstance): Promise { const con = await createOrGetConnection(); - fastify.get('/', (req, res) => - res.redirect('https://r.daily.dev/api-redirect'), - ); + fastify.get('/', (req, res) => { + // Some OAuth providers (e.g. GitHub) redirect cancellations to the + // API root instead of the BetterAuth callback URL. Forward the error + // params to the webapp callback so the popup can close cleanly. + const { error, state } = req.query as Record; + if (error && state) { + const webappOrigin = process.env.COMMENTS_PREFIX; + if (webappOrigin) { + req.log.warn( + { error, state }, + 'OAuth error redirected to API root, forwarding to webapp callback', + ); + const callbackUrl = new URL(`${webappOrigin}/callback`); + const params = new URLSearchParams(req.url.split('?')[1] ?? ''); + params.forEach((value, key) => { + callbackUrl.searchParams.set(key, value); + }); + return res.redirect(callbackUrl.toString()); + } + } + + return res.redirect('https://r.daily.dev/api-redirect'); + }); fastify.get('/landing', (req, res) => res.redirect('https://daily.dev')); fastify.get('/tos', (req, res) => res.redirect('https://daily.dev/tos')); fastify.get('/privacy', (req, res) => From 4cbcde5a3c7ca4f1cfee8b19bcbcfd7eb667698c Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 9 Apr 2026 11:05:28 +0200 Subject: [PATCH 2/2] fix: cleanup --- src/betterAuth.ts | 18 ++---------- src/routes/betterAuth.ts | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/betterAuth.ts b/src/betterAuth.ts index 00214dee51..5326c8bc71 100644 --- a/src/betterAuth.ts +++ b/src/betterAuth.ts @@ -287,32 +287,20 @@ const cookieDomain = process.env.BETTER_AUTH_BASE_URL ? extractRootDomain(new URL(process.env.BETTER_AUTH_BASE_URL).hostname) : undefined; -const getOAuthErrorRedirectUrl = (): string | undefined => { - const webappUrl = process.env.COMMENTS_PREFIX; - if (!webappUrl) { - return undefined; - } - return `${webappUrl}/callback`; -}; - export const getBetterAuthOptions = (pool: Pool): BetterAuthOptions => { const trustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS ? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(',') : []; - const oauthErrorRedirectUrl = getOAuthErrorRedirectUrl(); - return { database: pool, baseURL: process.env.BETTER_AUTH_BASE_URL || 'http://localhost:3000', basePath: '/auth', secret: process.env.BETTER_AUTH_SECRET ?? '', trustedOrigins, - ...(oauthErrorRedirectUrl && { - onAPIError: { - errorURL: oauthErrorRedirectUrl, - }, - }), + onAPIError: { + errorURL: `${process.env.COMMENTS_PREFIX}/callback`, + }, secondaryStorage: { get: (key) => singleRedisClient.get(`ba:${key}`), set: (key, value, ttl) => diff --git a/src/routes/betterAuth.ts b/src/routes/betterAuth.ts index a838204b1c..d0ac98b407 100644 --- a/src/routes/betterAuth.ts +++ b/src/routes/betterAuth.ts @@ -119,6 +119,57 @@ export const logoutBetterAuth = async ( } }; +const isOAuthCallbackPath = (url: string): boolean => + /\/auth\/callback\//.test(url); + +const rewriteOAuthErrorRedirect = ( + request: FastifyRequest, + response: Response, +): string | undefined => { + if (!isOAuthCallbackPath(request.url)) { + return undefined; + } + + if (response.status < 300 || response.status >= 400) { + return undefined; + } + + const location = response.headers.get('location'); + if (!location) { + return undefined; + } + + const webappCallback = `${process.env.COMMENTS_PREFIX}/callback`; + if (!webappCallback) { + return undefined; + } + + if (location.startsWith(webappCallback)) { + return undefined; + } + + let redirectUrl: URL; + try { + redirectUrl = new URL(location, `${request.protocol}://${request.host}`); + } catch { + return undefined; + } + + const hasError = + redirectUrl.searchParams.has('error') || + redirectUrl.searchParams.get('state') === 'state_not_found'; + if (!hasError) { + return undefined; + } + + const callbackUrl = new URL(webappCallback); + redirectUrl.searchParams.forEach((value, key) => { + callbackUrl.searchParams.set(key, value); + }); + + return callbackUrl.toString(); +}; + const betterAuthRoute = async (fastify: FastifyInstance): Promise => { // Apple sends OAuth callbacks as application/x-www-form-urlencoded POSTs. // Fastify does not parse this content type by default, so collect the raw @@ -142,6 +193,18 @@ const betterAuthRoute = async (fastify: FastifyInstance): Promise => { reply, body, }); + + const rewrittenUrl = rewriteOAuthErrorRedirect(request, response); + if (rewrittenUrl) { + request.log.warn( + { originalLocation: response.headers.get('location') }, + 'OAuth callback error redirect rewritten to webapp', + ); + reply.header('location', rewrittenUrl); + reply.status(302); + return reply.send(); + } + return sendBetterAuthResponse(reply, response); } catch (error) { return sendBetterAuthError(