Firebase auth/popup-closed-by-user, when it's really the user and when it isn't
auth/popup-closed-by-user means Firebase saw the sign-in window as closed before sign-in finished, which is usually a real cancel by the user.
If it fires while the popup is still open, check your page's Cross-Origin-Opener-Policy header; if users closed a popup that showed an error, fix that error. Either way, handle the code as a quiet cancel.
How Firebase decides the popup was closed
Firebase can't see why a window closed. It only checks whether it is closed. In the Firebase JS SDK source (as of Oct 2026):
- After opening the sign-in window, the SDK checks the window's
closedproperty right away, then about every 2 seconds in a browser. - Once
closedreadstrue, it waits another 8 seconds (_Timeout.AUTH_EVENT = 8000). A source comment explains why: "The window could have closed but the sign in network call could still be in flight." - If no sign-in result arrives in those 8 seconds, it rejects with
auth/popup-closed-by-userand the message "The popup has been closed by the user before finalizing the operation."
Two things follow:
- Any reason for
closedto readtruegives this code, including reasons that have nothing to do with the user. - The error arrives late. After a real close, expect it up to about 10 seconds later. Don't treat that delay as a bug.
When it really is the user
- The user closed the window before picking an account, often because they changed their mind.
- The user closed the window because it showed an error page. The SDK reports that as
auth/popup-closed-by-usertoo. Ask what the popup showed: - Google's "Error 400: redirect_uri_mismatch": see redirect_uri_mismatch.
- Firebase's "The requested action is invalid.": see The requested action is invalid.
- Your own app, a 404, or a blank page at
/__/auth/handler: yourauthDomainisn't serving Firebase's sign-in handler. See the Firebase auth handler. Firebase's docs say that if you route/__/auth/through your own domain, the forwarding must be "transparent to the browser; this cannot be done via a 302 Redirect."
These look like cancels in your logs, but they are setup problems.
When it isn't the user: Cross-Origin-Opener-Policy
The header Cross-Origin-Opener-Policy (COOP) controls whether your page and the windows it opens stay connected. Two MDN statements explain the problem:
- On
window.open(): if COOP opens the new document in a new browsing context group, "references to the opened window are severed and the returned object will indicate that the opened window is closed (closedistrue)." - On COOP values:
same-originonly shares a browsing context group with same-origin documents.same-origin-allow-popups"allows the opening of documents usingWindow.open()in the same BCG if they have a COOP value ofunsafe-none," and MDN names OAuth as a use case for it.unsafe-noneis the default.
Put that next to the SDK's check: if your page sends Cross-Origin-Opener-Policy: same-origin, the sign-in window can read as closed soon after it opens. A likely tell: auth/popup-closed-by-user arrives roughly 8 to 10 seconds after the popup opened, while the popup is still on screen. No Firebase doc states this link outright; it follows from MDN's description of COOP and the SDK's closed-window check.
Google gives the same advice for its own sign-in popups (Google Identity Services): when FedCM is off, set COOP to same-origin-allow-popups, because "Failing to set the proper header breaks communication between windows."
Fix the header
- Check what your page sends. In DevTools, open the Network tab, reload, select your page's document request and look under Response Headers. Or run:
bash
curl -sI https://your-app.example.com/ | grep -i cross-origin-opener-policy
No line means no COOP header, so COOP isn't your problem.
- If it says
same-origin, change it on the pages that callsignInWithPopup:
Cross-Origin-Opener-Policy: same-origin-allow-popups
On Firebase Hosting, custom headers go in firebase.json:
json
{
"hosting": {
"headers": [
{
"source": "**",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin-allow-popups" }
]
}
]
}
}
On other hosts, use your host's or framework's response-header setting. Narrow the source pattern to your sign-in pages if the rest of the site needs same-origin.
-
If your
authDomainis your own domain (you proxy or self-host/__/auth/), don't send a strict COOP header on/__/auth/*responses. Per MDN,same-origin-allow-popupsonly keeps the popup connected if the opened page's own COOP isunsafe-none(or absent). -
If a page must keep
same-origin, use redirect sign-in on that page. See popup or redirect before switching, because redirect sign-in has its own setup requirements.
Third-party storage is a different error
When the SDK finds that the storage its hidden helper frame needs isn't available, the source shows it rejects with auth/web-storage-unsupported, not auth/popup-closed-by-user. If you see that code, it's a storage problem, not a closed window.
Handle it gracefully in code
Treat auth/popup-closed-by-user as "the user backed out": reset the button, show no error banner, and don't jump to a redirect on their behalf. Log how long it took, so the COOP pattern stands out:
import {
getAuth, GoogleAuthProvider, signInWithPopup, AuthErrorCodes,
} from 'firebase/auth';
const auth = getAuth();
const provider = new GoogleAuthProvider();
signInButton.addEventListener('click', () => {
const startedAt = performance.now();
signInButton.disabled = true;
// Called first, with nothing awaited before it.
signInWithPopup(auth, provider)
.then((result) => {
showSignedIn(result.user);
})
.catch((error) => {
switch (error.code) {
case AuthErrorCodes.POPUP_CLOSED_BY_USER: {
// A cancel, not a failure. Keep the timing for debugging.
const seconds = Math.round((performance.now() - startedAt) / 1000);
console.info(`Sign-in window reported closed after ${seconds}s`);
break;
}
case AuthErrorCodes.EXPIRED_POPUP_REQUEST:
// auth/cancelled-popup-request: a newer popup replaced this one.
break;
case AuthErrorCodes.POPUP_BLOCKED:
showMessage('Allow pop-ups for this site, then try again.');
break;
default:
showMessage('Sign-in did not finish. Please try again.');
console.error(error.code, error.message);
}
})
.finally(() => {
signInButton.disabled = false;
});
});
AuthErrorCodes.POPUP_CLOSED_BY_USER is "auth/popup-closed-by-user" and AuthErrorCodes.EXPIRED_POPUP_REQUEST is "auth/cancelled-popup-request" in the Firebase JS reference. If you see the second code a lot, see auth/cancelled-popup-request.
How to confirm it worked
- Header fix: the curl command above shows
same-origin-allow-popups(or no COOP line), and sign-in completes without the 8-second error while the popup is open. - Error-page fix: the popup goes straight to Google's account chooser and closes itself after sign-in, and
signInWithPopupresolves withresult.user. - Code: closing the popup on purpose leaves the button usable and shows no error banner.
FAQ
Why does the error take several seconds to show up after I close the popup?
The SDK checks the window about every 2 seconds and then waits 8 more seconds after it sees the window closed, in case the sign-in request is still finishing. So up to about 10 seconds is normal.
Should I show an error message for auth/popup-closed-by-user?
Usually not. Treat it as a cancel and let the user try again. Do log it, because a spike can point to an error page in the popup or a COOP header.
How can I tell a COOP problem from a real cancel?
With COOP same-origin on your page, the error tends to arrive about 8 to 10 seconds after the popup opened, while the popup is still visible. Check your page's Cross-Origin-Opener-Policy response header to confirm.
Can blocked third-party cookies or storage cause this error?
Not directly. When the SDK detects that its helper frame can't use storage, it reports auth/web-storage-unsupported instead.
Does signInWithRedirect avoid this error?
Redirect sign-in doesn't open a popup, so there is no window to watch. It has its own setup requirements on current browsers, so read Firebase's redirect best practices before switching.
Sources
- Firebase JS SDK source: packages/auth/src/platform_browser/strategies/popup.ts (closed-window poll, 8-second wait, web-storage check)
- Firebase JS SDK source: packages/auth/src/core/util/delay.ts (poll delay used in browsers)
- Firebase JS SDK source: packages/auth/src/core/errors.ts (popup-closed-by-user message)
- Firebase JS API reference: firebase/auth (signInWithPopup, AuthErrorCodes)
- Firebase: Best practices for using signInWithRedirect on browsers that block third-party storage access
- Firebase Hosting: Configure Hosting behavior (headers)
- MDN: Window: open() method
- MDN: Cross-Origin-Opener-Policy header
- Google Identity Services: Get your Google API client ID (Cross Origin Opener Policy)
How this page was made: drafted with AI from the sources listed, reviewed by Pewterkit. Docs checked Oct 27, 2026. Spotted something out of date? Tell us.