From d4873a391680bd78b122ce86b6aaf90cb382deaa Mon Sep 17 00:00:00 2001 From: Roy Miloh Date: Tue, 24 Feb 2026 21:47:14 -0300 Subject: [PATCH 1/4] Use popup for OAuth on preview/sandbox/checkpoint domains On preview and sandbox domains the app runs inside an iframe, which causes OAuth providers to block the normal full-page redirect via X-Frame-Options. Instead, open the provider flow in a popup, poll until it lands back on our origin, then mirror that callback URL (including the access_token query param) in the iframe so it processes the token exactly as a normal redirect would. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/auth.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/modules/auth.ts b/src/modules/auth.ts index d186622..26bf02e 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -7,6 +7,63 @@ import { ResetPasswordParams, } from "./auth.types"; +const POPUP_AUTH_DOMAIN_REGEX = + /^(preview-sandbox--|preview--|checkpoint--)[^.]+\.base44\.app$/; + +function isPopupAuthDomain(): boolean { + if (typeof window === "undefined") return false; + return POPUP_AUTH_DOMAIN_REGEX.test(window.location.hostname); +} + +/** + * Opens a URL in a centered popup and, once the OAuth provider redirects + * back to our origin, mirrors that callback URL in the current window so the + * iframe processes the access_token query param exactly as a normal redirect + * would. + * + * @param url - The URL to open in the popup. + */ +function loginViaPopup(url: string): void { + const width = 500; + const height = 600; + const left = Math.round(window.screenX + (window.outerWidth - width) / 2); + const top = Math.round(window.screenY + (window.outerHeight - height) / 2); + + const popup = window.open( + url, + "base44_auth", + `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes` + ); + + if (!popup) { + return; + } + + const pollTimer = setInterval(() => { + if (popup.closed) { + clearInterval(pollTimer); + return; + } + + try { + // Accessing popup.location.href throws a cross-origin error while the + // OAuth provider's pages are open — that's expected and means the flow + // is still in progress. Once it stops throwing, the popup has landed + // back on our origin with the callback URL (e.g. ?access_token=...). + const callbackUrl = popup.location.href; + if (new URL(callbackUrl).origin === window.location.origin) { + clearInterval(pollTimer); + popup.close(); + // Redirect the iframe to the same URL the popup landed on so it + // processes the token from the query params as it normally would. + window.location.href = callbackUrl; + } + } catch { + // Still on the OAuth provider's domain — keep polling + } + }, 300); +} + /** * Creates the auth module for the Base44 SDK. * @@ -68,7 +125,13 @@ export function createAuthModule( redirectUrl )}`; - // Redirect to the provider login page + // On preview/sandbox/checkpoint domains the app runs inside an iframe — + // use a popup to avoid OAuth providers blocking iframe navigation. + if (isPopupAuthDomain()) { + return loginViaPopup(loginUrl); + } + + // Default: full-page redirect window.location.href = loginUrl; }, From 89ca60a7a69971f1612d29d74b5797fe57c0b0df Mon Sep 17 00:00:00 2001 From: Roy Miloh Date: Tue, 24 Feb 2026 23:41:02 -0300 Subject: [PATCH 2/4] any domain --- src/modules/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 26bf02e..afe91bb 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -8,7 +8,7 @@ import { } from "./auth.types"; const POPUP_AUTH_DOMAIN_REGEX = - /^(preview-sandbox--|preview--|checkpoint--)[^.]+\.base44\.app$/; + /^(preview-sandbox--|preview--|checkpoint--)[^.]+\./; function isPopupAuthDomain(): boolean { if (typeof window === "undefined") return false; From 4a3ddcf51522b9e20f25c8b782b2a37fe2694100 Mon Sep 17 00:00:00 2001 From: Roy Miloh Date: Wed, 25 Feb 2026 15:35:30 -0300 Subject: [PATCH 3/4] Switch popup auth to postMessage instead of location polling Replace the cross-origin location polling hack with a proper postMessage listener. The backend now sends { access_token, is_new_user } via postMessage when popup_origin is passed as a query param. On receipt, the iframe redirects to fromUrl with the token params appended, preserving identical behaviour to the normal OAuth callback redirect. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/auth.ts | 77 ++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 38a2686..a4d6c66 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -16,14 +16,20 @@ function isPopupAuthDomain(): boolean { } /** - * Opens a URL in a centered popup and, once the OAuth provider redirects - * back to our origin, mirrors that callback URL in the current window so the - * iframe processes the access_token query param exactly as a normal redirect - * would. + * Opens a URL in a centered popup and waits for the backend to postMessage + * the auth result back. On success, redirects the current window to + * redirectUrl with the token params appended, preserving the same behaviour + * as a normal full-page redirect flow. * - * @param url - The URL to open in the popup. + * @param url - The login URL to open in the popup (should include popup_origin). + * @param redirectUrl - Where to redirect after auth (the original fromUrl). + * @param expectedOrigin - The origin we expect the postMessage to come from. */ -function loginViaPopup(url: string): void { +function loginViaPopup( + url: string, + redirectUrl: string, + expectedOrigin: string +): void { const width = 500; const height = 600; const left = Math.round(window.screenX + (window.outerWidth - width) / 2); @@ -39,29 +45,39 @@ function loginViaPopup(url: string): void { return; } - const pollTimer = setInterval(() => { - if (popup.closed) { - clearInterval(pollTimer); - return; - } + const cleanup = () => { + window.removeEventListener("message", onMessage); + clearInterval(pollTimer); + if (!popup.closed) popup.close(); + }; - try { - // Accessing popup.location.href throws a cross-origin error while the - // OAuth provider's pages are open — that's expected and means the flow - // is still in progress. Once it stops throwing, the popup has landed - // back on our origin with the callback URL (e.g. ?access_token=...). - const callbackUrl = popup.location.href; - if (new URL(callbackUrl).origin === window.location.origin) { - clearInterval(pollTimer); - popup.close(); - // Redirect the iframe to the same URL the popup landed on so it - // processes the token from the query params as it normally would. - window.location.href = callbackUrl; - } - } catch { - // Still on the OAuth provider's domain — keep polling + const onMessage = (event: MessageEvent) => { + if (event.origin !== expectedOrigin) return; + if (event.source !== popup) return; + if (!event.data?.access_token) return; + + cleanup(); + + // Append the token params to redirectUrl so the app processes them + // exactly as it would from a normal OAuth callback redirect. + const callbackUrl = new URL(redirectUrl); + const { access_token, is_new_user } = event.data; + + callbackUrl.searchParams.set("access_token", access_token); + + if (is_new_user != null) { + callbackUrl.searchParams.set("is_new_user", String(is_new_user)); } - }, 300); + + window.location.href = callbackUrl.toString(); + }; + + // Only used to detect the user closing the popup before auth completes + const pollTimer = setInterval(() => { + if (popup.closed) cleanup(); + }, 500); + + window.addEventListener("message", onMessage); } /** @@ -134,7 +150,12 @@ export function createAuthModule( // On preview/sandbox/checkpoint domains the app runs inside an iframe — // use a popup to avoid OAuth providers blocking iframe navigation. if (isPopupAuthDomain()) { - return loginViaPopup(loginUrl); + const popupLoginUrl = `${loginUrl}&popup_origin=${encodeURIComponent(window.location.origin)}`; + return loginViaPopup( + popupLoginUrl, + redirectUrl, + new URL(options.appBaseUrl).origin + ); } // Default: full-page redirect From d956672f1912f35354f81f2e3723995335bb2c87 Mon Sep 17 00:00:00 2001 From: Roy Miloh Date: Wed, 25 Feb 2026 16:31:38 -0300 Subject: [PATCH 4/4] Fix postMessage origin check to use current window origin The popup sends its message after the backend redirects it back to our app's domain, so event.origin is window.location.origin, not the Base44 server's origin. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/auth.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/auth.ts b/src/modules/auth.ts index a4d6c66..ab49cf7 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -151,11 +151,7 @@ export function createAuthModule( // use a popup to avoid OAuth providers blocking iframe navigation. if (isPopupAuthDomain()) { const popupLoginUrl = `${loginUrl}&popup_origin=${encodeURIComponent(window.location.origin)}`; - return loginViaPopup( - popupLoginUrl, - redirectUrl, - new URL(options.appBaseUrl).origin - ); + return loginViaPopup(popupLoginUrl, redirectUrl, window.location.origin); } // Default: full-page redirect