HubSpot OAuth integration fails to return access tokens when user authenticates during flow

I’m working on connecting HubSpot OAuth to my web app and running into a weird problem. Everything works great if the user is already signed into their HubSpot account before starting the OAuth process. But if they need to log in first during the OAuth flow, my app doesn’t get the access tokens back.

const authResponse = await fetch('https://api.hubapi.com/oauth/v1/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: formData.toString()
});

const authData = { ...authResponse.data, authCode };
console.log('Auth data received:', authData);

return res.status(200).send(
  `<html>
    <head><title>HubSpot Connected</title></head>
    <body>
      <h1>Connection Successful!</h1>
      <p>Please close this tab.</p>
      <button onclick="window.close()">Close Tab</button>
      <script>
        if (window.opener) {
          window.opener.postMessage(${JSON.stringify(authData)}, "http://localhost:3000/settings");
        }
      </script>
    </body>
  </html>`
);

The problem only happens when users have to authenticate with HubSpot first. My app opens a popup window for the OAuth flow but after they log in, the token exchange doesn’t work properly. I’ve tried checking the server logs and the token request is being made. I also verified my redirect URLs and app permissions are set up correctly. The tokens do get retrieved but somehow the popup window can’t communicate back to the main application window.

sounds like a timing issue. maybe the page is reloading or redirecting too fast for your postMessage to run. try adding a small delay or wait for the page to finish loading before you send the message. also check if window.opener is available after the HubSpot login.

I’ve hit this exact same issue with HubSpot OAuth. The problem is HubSpot’s login bounces through multiple redirects and sometimes changes the window context, which kills the window.opener reference. Here’s what actually worked for me: use localStorage as a backup communication method. When the OAuth callback runs, dump the auth data into localStorage with a unique key, then poll for that data from your parent window. You could also try the BroadcastChannel API. Bottom line - you need a backup since postMessage breaks when users go through the full auth flow instead of just the authorization step.