Skip to content

horizon -> mainmast#617

Merged
jpolitz merged 4 commits intomainmastfrom
horizon
Feb 2, 2026
Merged

horizon -> mainmast#617
jpolitz merged 4 commits intomainmastfrom
horizon

Conversation

@jpolitz
Copy link
Member

@jpolitz jpolitz commented Feb 2, 2026

No description provided.

…Storage polling

@schanzer and I were on a call with a school that was getting bad login
behavior (blank index page after successfully entering Google credentials,
blank definitions area when logged in [but shared files would load; Google
connection the issue]). It seems like GoGuardian is to blame.

In their dev tools we saw “Cross-Origin-Opener-Policy would block the
window.opener call” in the gapi library. My guess/investigation is that some
of the communication between the login popup and the home page was failing. I
was able to partially reproduce this myself by setting
Cross-Origin-Opener-Policy in Chrome devtools and by explicitly adding this to
server.js:

  app.use(function(req, res, next) {
    res.setHeader("Cross-Origin-Opener-Policy", "noopener-allow-popups");
    next();
  });

(“Partially” because I got a spinning “waiting for login” with the failure
rather than blank, but I'm making a [big?] assumption that these issues are
similar enough that this is a good idea.)

I think the change here is a strict improvement anyway and could get around
some annoying same-origin issues.

This says [ai-assisted] because I asked Claude for help, transcript here:

❯ OK here's a debugging issue. We have a school with GoGuardian installed trying to use Pyret (in
  particular the signin from the index dashboard page in code.pyret.org/src/web/). The login popup
  opens, Google Login apparently succeeds. However, the main page shows as blank. From debugging
  (over Zoom, with a non-expert tech person navigating dev tools), it looks like something (I'm
  tempted to blame GoGuardian), may be inserting Cross-Origin-Opener-Polic headers. We see the
  error message "Cross-Origin-Opener-Policy would block the window.opener call", pointing to a
  line number inside the Google Client library.

  I have a few ideas, but I'd like to hear more and have mine evaluated

  - Try to do something other than window.opener, like setting something in localStorage and
  polling for it
  - Learn more about what GoGuardian is doing (this has been really hard to do). Is there a way to
  install GoGuardian in a test mode on a dev machine without installing it in some
  institution-wide way?
  - Try to use a different Google API for login – this is a bit fraught because we have 10s of
  thousands of existing users who are all happily logging in, so I'm very wary of breaking
  something else

  I've been able to reproduce the same console error message by overriding headers with
  Cross-Origin-Opener-Policy: noopener-allow-popups in Chrome devtools on the live
  https://code.pyret.org site. However, the login apparently succeeds when I do that (I'm not sure
  I'm exactly reproducing everything GoGuardian does).

  I'd welcome ideas, successful searches of similar issues, feedback on my ideas, and so on

⏺ This is a well-documented class of problem. Let me do some research on both the current Pyret
  implementation and known solutions for COOP + Google Sign-In conflicts.

⏺ 2 agents finished (ctrl+o to expand)
   ├─ Task (Research COOP Google Sign-In issues) · 12 tool uses · 22.6k tokens
   │  ⎿  Done
   └─ Explore (Explore Pyret login implementation) · 33 tool uses · 57.1k tokens
      ⎿  Done

⏺ This is very helpful research. Let me synthesize the findings and evaluate your ideas.

  Current Implementation Analysis

  Your auth flow in code.pyret.org/src/web/js/google-apis/api-wrapper.js:47-77 uses a popup that
  communicates via postMessage:

  // close.html does this:
  window.opener.postMessage("done", document.location.origin);
  window.close();

  This is the exact line that breaks when COOP headers sever window.opener.

  ---
  Evaluation of Your Ideas

  1. localStorage + polling (or BroadcastChannel)

  Verdict: Good practical workaround

  This is the right direction. BroadcastChannel is even better than polling:

  // In close.html (popup)
  localStorage.setItem('pyret_auth_complete', Date.now());
  const channel = new BroadcastChannel('pyret_auth');
  channel.postMessage({ type: 'auth_complete' });
  window.close();

  // In api-wrapper.js (opener)
  const channel = new BroadcastChannel('pyret_auth');
  channel.onmessage = (e) => {
    if (e.data.type === 'auth_complete') {
      resolve(reauth(true, useFullScopes));
    }
  };
  // Fallback: also listen for storage event
  window.addEventListener('storage', (e) => {
    if (e.key === 'pyret_auth_complete') { ... }
  });

  Pros: Works regardless of COOP, same-origin so no security issues, minimal code change
  Cons: Need to handle cleanup, slightly different timing characteristics

  2. Learn more about GoGuardian

  Verdict: Low ROI

  GoGuardian runs as a Chrome extension + proxy in managed Chromebook environments. You can't easily
  install it for testing without institutional management. Their release notes mention they've been
  working on header handling issues, but you can't control what they do.

  Better approach: Your DevTools header override is actually a reasonable approximation for testing.
  You could also test with a simple extension that injects COOP headers.

  3. Different Google API

  Verdict: Actually the best long-term solution

  You're currently using the deprecated gapi client library (apis.google.com/js/client.js). Google is
  actively migrating to Google Identity Services (GIS) with FedCM support.

  Key insight from research: GIS with FedCM enabled renders authentication UI directly in the
  browser—no popup communication needed at all. This completely sidesteps the COOP problem.

  Google's migration timeline: April 2025 onward, GIS is auto-migrating to FedCM.

  ---
  Recommended Approach (Layered)

  Short-term fix (days): BroadcastChannel fallback

  Modify close.html and api-wrapper.js to use BroadcastChannel with postMessage as fallback:

  // close.html - try both methods
  if (window.opener) {
    window.opener.postMessage("done", document.location.origin);
  }
  // Always also use BroadcastChannel (works even if opener is null)
  const channel = new BroadcastChannel('pyret_auth');
  channel.postMessage({ type: 'done' });
  channel.close();
  window.close();

  Medium-term (weeks): Migrate to Google Identity Services

  This is worth doing anyway since gapi is deprecated. GIS has specific COOP guidance:
  - With FedCM enabled: No COOP changes needed
  - Without FedCM: Requires Cross-Origin-Opener-Policy: same-origin-allow-popups

  Also consider: Redirect flow option

  For problematic environments, offer a redirect-based auth flow instead of popup. Your server-side
  OAuth (/login, /oauth/callback) already supports this—you'd just change the redirect parameter to
  return to the editor instead of close.html.

  ---
  Testing GoGuardian-like behavior

  Create a simple test extension:

  // manifest.json
  {
    "manifest_version": 3,
    "name": "COOP Test",
    "version": "1.0",
    "permissions": ["declarativeNetRequest"],
    "declarative_net_request": {
      "rule_resources": [{
        "id": "rules",
        "enabled": true,
        "path": "rules.json"
      }]
    }
  }

  // rules.json
  [{
    "id": 1,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "responseHeaders": [{
        "header": "Cross-Origin-Opener-Policy",
        "operation": "set",
        "value": "same-origin"
      }]
    },
    "condition": {
      "urlFilter": "*://code.pyret.org/*",
      "resourceTypes": ["main_frame"]
    }
  }]

  This will let you reliably reproduce the issue locally.

  ---
  Key Files to Modify
  File: code.pyret.org/src/web/close.html
  Change: Add BroadcastChannel alongside postMessage
  ────────────────────────────────────────
  File: code.pyret.org/src/web/js/google-apis/api-wrapper.js:47-77
  Change: Listen on BroadcastChannel in reauth()
  ────────────────────────────────────────
  File: code.pyret.org/src/web/editor.html
  Change: Eventually: migrate from gapi to GIS
  Want me to draft the BroadcastChannel changes as a concrete starting point?

✻ Crunched for 3m 10s

❯ yes
- Reorder removing a key with setting a handler to avoid a spurious event
- Make sure cleanup clears localStorage and nulls out the channel
- Add some useful debugging info we can look for in console.logs

I tested this by commenting out cleanup and checking that all 3 modes triggered.

Then I added

+++ b/src/server.js
@@ -45,6 +45,11 @@ function start(config, onServerReady) {
   app = express();
   app.use(bodyParser.urlencoded({ extended: false }))
   app.use(bodyParser.json())
+  app.use(function(req, res, next) {
+    res.setHeader("Cross-Origin-Opener-Policy", "noopener-allow-popups");
+    next();
+  });
+

And verified that the postMessage failed, but the other two mechanisms succeed
[ai-assisted] Work around COOP issues with BroadcastChannel and local…
@jpolitz jpolitz merged commit 7248384 into mainmast Feb 2, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant