Firebase auth/popup-closed-by-user, when it's really the user and when it isn't

Updated 26 Oct 2026 · Pewterkit Guides

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 closed property right away, then about every 2 seconds in a browser.
  • Once closed reads true, 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-user and the message "The popup has been closed by the user before finalizing the operation."

Two things follow:

  1. Any reason for closed to read true gives this code, including reasons that have nothing to do with the user.
  2. 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-user too. 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: your authDomain isn'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 (closed is true)."
  • On COOP values: same-origin only shares a browsing context group with same-origin documents. same-origin-allow-popups "allows the opening of documents using Window.open() in the same BCG if they have a COOP value of unsafe-none," and MDN names OAuth as a use case for it. unsafe-none is 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

  1. 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.

  1. If it says same-origin, change it on the pages that call signInWithPopup:

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.

  1. If your authDomain is your own domain (you proxy or self-host /__/auth/), don't send a strict COOP header on /__/auth/* responses. Per MDN, same-origin-allow-popups only keeps the popup connected if the opened page's own COOP is unsafe-none (or absent).

  2. 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

  1. 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.
  2. Error-page fix: the popup goes straight to Google's account chooser and closes itself after sign-in, and signInWithPopup resolves with result.user.
  3. 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

  1. Firebase JS SDK source: packages/auth/src/platform_browser/strategies/popup.ts (closed-window poll, 8-second wait, web-storage check)
  2. Firebase JS SDK source: packages/auth/src/core/util/delay.ts (poll delay used in browsers)
  3. Firebase JS SDK source: packages/auth/src/core/errors.ts (popup-closed-by-user message)
  4. Firebase JS API reference: firebase/auth (signInWithPopup, AuthErrorCodes)
  5. Firebase: Best practices for using signInWithRedirect on browsers that block third-party storage access
  6. Firebase Hosting: Configure Hosting behavior (headers)
  7. MDN: Window: open() method
  8. MDN: Cross-Origin-Opener-Policy header
  9. 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 26 Oct 2026. Spotted something out of date? Tell us.

Related guides