Search code examples
javascriptoauthpromiserace-conditionpostmessage

How to avoid race condition between `addEventListener` and `window.open`


As part of the OAuth flow for my site, I'm using window.open to allow the user to sign in to the 3rd party provider, and postMessage to send the auth token back to my webpage. The basic flow looks something like this, though I've removed the extra code related to timeouts, cleanups, error handling, and so on:

const getToken = () => new Promise((resolve, reject) => {
    const childWindow = window.open(buildUrl({
        url: "https://third-party-provider.com/oauth/authorize",
        query: {
            redirect_uri: "my-site.com/oauth-callback"
        }
    })

    window.addEventListener('message', event => {
        if(
            event.origin === window.location.origin &&
            event.source === childWindow
        ) {
            if(event.data.error) reject(event.data.error)
            else resolve(event.data)
        }
    })

    // Plus some extra code to remove the event listener and close
    // the child window.
}

The basic idea is that, when the user clicks authorize, they are redircted to my-site.com/oauth-callback. That page completes the OAuth flow on the server side, and then loads a page containing a small amount of javascript which simply calls window.opener.postMessage(...)

Now, the crux of my question is that there's actually a race condition here, at least in theory. The issue is that the childWindow could hypothetically call postMessage before addEventListener is called. Conversely, if I call addEventListener first, it could receive messages before childWindow is set, which I think will throw and exception? The key issue seems to be that window.open isn't asynchronous, so I can't atomically spawn a window and set up a message handler.

Currently, my solution is to set up the event handler first, and assume that messages don't come in while that function is being executed. Is that a reasonable assumption? If not, is there a better way to ensure this flow is correct?


Solution

  • There shouldn't be a race condition here, because the window.open and window.addEventListener occur together in the same tick, and Javascript is both single-threaded and non-reentrant.

    If the child window does manage to posts a message before the call to window.open completes, the message will simply go into the event queue for the current Javascript context. Your code is still running, so the addEventListener call happens next. Finally, at some future point that we don't see here, your code returns control to the browser, which ends the current tick.

    Once the current tick is over, the browser can check out the event queue and dispatch the next piece of work (presumably the message). Your subscriber is already in place, so everything is fine!