Login doesn't stick in an iframe
A login inside a cross-site iframe usually fails because the browser does not send or store the session cookie. Safari blocks third-party cookies, Firefox partitions them, and Chrome needs SameSite=None and Secure. Many login providers also refuse to load in a frame.
6 min read · Updated
The symptoms
- The user signs in inside the frame, and the next page shows the login form again.
- Login works in Chrome but not in Safari, or not in a private window.
- The frame goes blank or shows "refused to connect" when it reaches the login step.
- Login works on your own site, but not inside the partner's page.
All of these come from one fact: inside https://partner.example, your app at https://your-site.com is a third party.
Find the cause
Open DevTools in the frame context and sign in. Then check:
| What you see | Cause |
|---|---|
Set-Cookie in the response, but no Cookie on the next request | The browser blocked or did not send the cookie |
| A yellow warning next to the cookie in the Network or Application panel | The cookie lacks SameSite=None or Secure |
The frame goes to accounts.google.com, github.com/login, or another login provider, then stops | The login page blocks framing |
| A form that does nothing when submitted | A sandbox without allow-forms |
| A "Sign in with..." button that does nothing | A sandbox without allow-popups |
The error pages show the exact message of each cause: Login loop in an iframe, Set-Cookie blocked: SameSite=Lax, Third-party cookie blocked in Chrome, Blocked form submission, and Blocked opening in a new window.
Cause 1: the cookie is not sent
Set the session cookie with SameSite=None and Secure:
Set-Cookie: session=abc123; Path=/; Secure; HttpOnly; SameSite=None
Without SameSite=None, Chrome treats the cookie as Lax and does not send it in a cross-site frame. Without Secure, the browser rejects a SameSite=None cookie.
This fixes Chrome with default settings. It does not fix Safari, Chrome Incognito, or users who block third-party cookies. Firefox stores the cookie, but in a separate store for the partner site.
Cause 2: the browser blocks third-party cookies
You have four options. Pick by how much you control:
- Same site. Serve the app from a subdomain of the parent, for example
app.partner.example. The cookie is then first-party. This is the most reliable fix when you control both sites. - Partitioned cookie. Add
Partitionedto the cookie. The browser keeps a separate login for each top site, and the user signs in once inside the frame. See Cookies inside iframes. - Storage Access API. The frame asks for access to its normal cookies after a click. Then a user who is signed in on your site stays signed in inside the frame.
- Tokens, not cookies. The parent page gets a short-lived token from your API and sends it with postMessage. The frame keeps it in memory and sends it in an
Authorizationheader.
Cause 3: the login page refuses to load in a frame
Google, GitHub, Facebook, and other login providers send X-Frame-Options or frame-ancestors on their sign-in pages. They do this to stop clickjacking and password theft. You cannot change it.
Open the login in a popup:
// In the framed page, from a click handler
const popup = window.open("/auth/start", "login", "width=500,height=650");
window.addEventListener("message", (event) => {
if (event.origin !== window.location.origin) return;
if (event.source !== popup) return;
if (event.data?.type === "login:done") {
location.reload();
}
});
// On /auth/done, after the provider sends the user back
window.opener?.postMessage({ type: "login:done" }, window.location.origin);
window.close();
Do not send Cross-Origin-Opener-Policy: same-origin from /auth/start or /auth/done. It cuts the link between the windows, and window.opener becomes null.
The popup is a top-level window, so the provider's page loads. The popup sets the cookie as first-party. Safari and Firefox may still keep that cookie out of the frame. Combine the popup with the Storage Access API, a Partitioned cookie set by the frame, or a token.
Your own login page has the same rule. If it sends frame-ancestors 'self', it cannot load inside the partner page.
Cause 4: the sandbox removes what login needs
A sandboxed frame needs these flags for most login flows:
<iframe
src="https://your-site.com/app"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-storage-access-by-user-activation"
title="Your app"
></iframe>
Without allow-same-origin, the frame has a unique origin and cannot read any cookie. Without allow-popups-to-escape-sandbox, the login popup is sandboxed too. See The sandbox attribute.
Check the frame first
Paste the URL of your app and of your login page into the iframe tester. If either one cannot load in a frame, fix that before you work on cookies.