GuidesCROSS-ORIGIN-OPENER-POLICY
Cross-Origin-Opener-Policy: popups and SharedArrayBuffer
Cross-Origin-Opener-Policy (COOP) decides whether a popup stays connected to the page that opened it. We tested 21 popup setups in Chromium 153, Firefox 155 and WebKit 26.6 on 26 October 2026: with same-origin on your page, a popup on another origin lost window.opener, popup.closed read true and postMessage went nowhere, which is what breaks Google and Firebase sign-in popups. The red "would block the window.postMessage call" line is something else: a report-only warning caused by Google's own sign-in pages that blocks nothing. SharedArrayBuffer needs same-origin plus Cross-Origin-Embedder-Policy (COEP) on HTTPS or localhost.
The would block message is only a warning
Chromium 153 prints these lines at error level:
Cross-Origin-Opener-Policy policy would block the window.closed call.Cross-Origin-Opener-Policy policy would block the window.postMessage call.
Both come from Cross-Origin-Opener-Policy-Report-Only, a header that asks the browser to report what COOP would break without enforcing it. On 26 October 2026, Google's OAuth endpoint accounts.google.com/o/oauth2/v2/auth and the sign-in page it redirects to both sent Cross-Origin-Opener-Policy-Report-Only: same-origin with a report-to endpoint, and no enforced COOP.
We put the same header on our popup page. Nothing was blocked in any engine: popup.closed stayed false and messages arrived in both directions. Chromium logs the line in the window that made the access and names the first access it catches. Our opener logged window.closed at its first popup.closed read (Firebase's signInWithPopup polls this property), the popup logged window.postMessage for its window.opener.postMessage call, and the opener's later postMessage logged nothing. Firefox 155 and WebKit 26.6 printed nothing. You can't remove a warning that Google's page causes, and you don't need to.
A real COOP cut is silent: no engine logged anything. popup.closed was false right after window.open() and turned true once the popup loaded the other origin, while the popup window was still open. In the popup, window.opener was null, so it had no way to answer; the opener's popup.postMessage() threw no error and delivered nothing.
What each COOP value does to a popup
| Header and value | Sent by | Popup keeps window.opener | postMessage and popup.closed | Isolated with COEP |
|---|---|---|---|---|
| None, or unsafe-none | Your page | Yes | Work both ways | No |
| same-origin-allow-popups | Your page | Yes, while the pages in the popup send no COOP | Work both ways | No |
| same-origin | Your page | No; only a same-origin popup that also sends same-origin keeps it | Dropped without an error; popup.closed reads true once the popup loads | Yes with require-corp; with credentialless only in Chromium and Firefox |
| noopener-allow-popups | Your page | Yes, while the pages in the popup send no COOP | Work both ways | No |
| same-origin or same-origin-allow-popups on the popup page | The popup, another origin | No, whatever your page sends | Dropped without an error | Not applicable |
| noopener-allow-popups on the popup page | The popup, your own origin | No in Chromium and WebKit; Firefox 155 ignores the value | Dropped where supported | Not applicable |
| Cross-Origin-Opener-Policy-Report-Only: same-origin | Either page; Google's sign-in pages send it | Yes | Work; Chromium logs the would block errors | No |
Every page that loads inside the popup counts, not only the first one. With same-origin-allow-popups on our opener, the popup went to another origin and came back to a page on our origin: the link was cut when that return page also sent same-origin-allow-popups, and kept when it sent no COOP. A page of ours with same-origin-allow-popups loaded in the popup before the other origin cut it too, and a cut link stayed cut. This is the OAuth trap: the opener is set up correctly, but the callback route inside the popup gets the site-wide COOP header.
noopener-allow-popups is newer: MDN's compatibility data lists Chrome 131 and Safari 18.4, not Firefox. On a same-origin popup page it cut the link in Chromium 153 and WebKit 26.6, and Firefox 155 ignored it.
Fix Google and Firebase sign-in popups
- Use same-origin-allow-popups on the page that opens the popup
sending no COOP works too. Google's setup guide (last updated 6 April 2026) says nothing needs to change when FedCM is enabled, because the browser draws the popup itself; with FedCM disabled it says to set COOP to
same-origin"and includesame-origin-allow-popups". In practice that means the valuesame-origin-allow-popups: plainsame-origincut the popup in all 3 engines. The guide warns that the wrong header leads "to a blank pop-up window or similar bugs". - Send no COOP on pages that load inside the popup
an OAuth callback route, or a proxied
/__/auth/handlerwhen Firebase'sauthDomainis your own domain. Exclude these paths from site-wide header rules. Firebase's default handler onfirebaseapp.comsent no COOP on the 2 projects we checked. - Override the helmet default in Express
helmet()8.3.0 sendsCross-Origin-Opener-Policy: same-originunless you passcrossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }. We confirmed the override with Express 5.3.0.
If signInWithPopup fails with auth/popup-closed-by-user while the sign-in window is still open, check COOP first. The Firebase JS SDK checks popup.closed every 2,000 ms in a browser; once it reads true, the SDK waits 8,000 ms and rejects. Under same-origin, popup.closed reads true as soon as the popup reaches Google. signInWithRedirect avoids the popup; Firebase lists what it needs on browsers that block third-party storage in its redirect best practices.
SharedArrayBuffer is not defined
SharedArrayBuffer only exists when self.crossOriginIsolated is true. Without it, new SharedArrayBuffer(8) threw ReferenceError: SharedArrayBuffer is not defined in Chromium 153 and Firefox 155, and ReferenceError: Can't find variable: SharedArrayBuffer in WebKit 26.6. The header combinations we tried gave a clear rule:
Cross-Origin-Opener-Policy: same-originplusCross-Origin-Embedder-Policy: require-corpisolated the page in all 3 engines.same-originplusCross-Origin-Embedder-Policy: credentiallessisolated it in Chromium and Firefox, not in WebKit 26.6. MDN listscredentiallessfor Chrome 96 and Firefox 119, not Safari.- Either header alone did not, and neither did
same-origin-allow-popups,noopener-allow-popupsor a report-only header in the pair. - The page must be a secure context: HTTPS,
localhostor127.0.0.1. Onhttp://app.test, like a LAN address,isSecureContextwasfalseand nothing was isolated; Chromium logged "The Cross-Origin-Opener-Policy header has been ignored, because the URL's origin was untrustworthy."
Isolation needs same-origin, so it can't share a page with cross-origin popups. Isolate only the routes that need SharedArrayBuffer and keep same-origin-allow-popups everywhere else. On isolated routes, require-corp blocks cross-origin images and scripts without Cross-Origin-Resource-Policy, and iframes that don't send COEP as well; the errors and fixes are in net::ERR_BLOCKED_BY_RESPONSE explained.
import { defineConfig } from 'vite';
// Dev and preview servers only: your production host must send the same two headers
const isolationHeaders = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
};
export default defineConfig({
server: { headers: isolationHeaders },
preview: { headers: isolationHeaders },
});// next.config.mjs: on /editor the later rule wins, every other route keeps same-origin-allow-popups
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin-allow-popups' }],
},
{
source: '/editor/:path*',
headers: [
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
{ key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
],
},
];
},
};
export default nextConfig;Both configs ran on 26 October 2026. Vite 8.3.0 isolated vite dev and vite preview in all 3 engines; those settings never reach your production host. With Next.js 16.4.3, / sent same-origin-allow-popups and /editor was isolated in Chromium and Firefox. On Firebase Hosting, add the same pairs under hosting.headers in firebase.json, with a source glob for the routes that need them. On Netlify and Cloudflare Pages, add a _headers file to the publish directory: a path line such as /editor/* with the header lines indented below it.
Check your page
Paste this into the DevTools console of the page that opens the popup or needs SharedArrayBuffer. It requests the same URL again and prints the COOP and COEP headers next to isSecureContext, crossOriginIsolated and typeof SharedArrayBuffer. We ran it in all 3 engines on a plain page and on an isolated one.
// Paste into the DevTools console of the page that opens the popup or needs SharedArrayBuffer
const response = await fetch(location.href, { method: 'HEAD', cache: 'no-store' });
const header = (name) => response.headers.get(name) ?? '(not sent)';
console.table({
'Cross-Origin-Opener-Policy': header('Cross-Origin-Opener-Policy'),
'Cross-Origin-Opener-Policy-Report-Only': header('Cross-Origin-Opener-Policy-Report-Only'),
'Cross-Origin-Embedder-Policy': header('Cross-Origin-Embedder-Policy'),
isSecureContext: self.isSecureContext,
crossOriginIsolated: self.crossOriginIsolated,
SharedArrayBuffer: typeof SharedArrayBuffer,
});Good questions.
Is "would block the window.postMessage call" an error?
No. Chromium prints it in red, but it comes from Cross-Origin-Opener-Policy-Report-Only on Google's sign-in pages and nothing is blocked. In our test, postMessage still worked in both directions.
Why does signInWithPopup fail with auth/popup-closed-by-user?
Your page probably sends Cross-Origin-Opener-Policy: same-origin, so popup.closed reads true once the popup reaches Google. Firebase checks it every 2,000 ms and rejects 8,000 ms later. Send same-origin-allow-popups instead.
Which COOP value should I use for Google sign-in?
same-origin-allow-popups, or no COOP header, on the page that opens the popup, and no COOP on any page of yours that loads inside the popup.
Why is SharedArrayBuffer not defined?
The page is not cross-origin isolated. Serve it over HTTPS or localhost with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp, then check that self.crossOriginIsolated is true.
Does Safari support COEP credentialless?
Not in our test: WebKit 26.6 stayed un-isolated with credentialless, while Chromium 153 and Firefox 155 isolated the page. Use require-corp if Safari users need SharedArrayBuffer.
Do COOP and COEP block CORS requests?
No. From an isolated page, fetch('https://proxy.cors.dev/https://xkcd.com/info.0.json', { credentials: 'omit' }) returned 200 in all 3 engines. COEP only blocks cross-origin images and scripts that have neither CORS nor Cross-Origin-Resource-Policy, and iframes without COEP.