How to Use window.postMessage for Cross-Origin Communication Between iframes and Chrome Extensions

How to Use window.postMessage for Cross-Origin Communication Between iframes and Chrome Extensions featured image

Background

We have a Chrome Extension that authenticates with Google. On an applet that is essentially an iFrame inside a web application, we need to retrieve the Google token. Once the token is received, we can use the gapi to create files in the Google account for the authenticated user.

The Problem

To do this, when the user clicks the "Get started" button, an eventListener is triggered to communicate to the Chrome Extension. This gets a little complicated because this is an "applet", which is a web page that we do have control over, but inside an iFrame in a web application page that we don't have control over. To make it even more complicated, we need access to the Chrome.runtime to get the data we need from the Chrome Extension. To do this, the script needs to run from the Chrome Extension and is then injected into the web page.

Our first instinct was to have the addEventListener inside the Chrome Extension content.js, like so:

content.js
const iframe = window.document.getElementById("myapplet-iframe")

iframe.addEventListener("load", function() {
  const button = iframe.contentWindow.document.getElementById("get-started")
  
  if (!button) return alert("Button not found!");
  
  button.addEventListener('click', () => {
    alert("ive been clicked!")
  });
});

but that resulted in a dreaded cross-origin error: "Uncaught DOMException: Blocked a frame with origin 'https://canvas.instructure.com' from accessing a cross-origin frame" and the Chrome Extension receives error like this: "Uncaught SecurityError: Blocked a frame with origin 'https://canvas.instructure.com' from accessing a frame with origin 'https://my-lti.com'. Protocols, domains, and ports must match". Yikes, we hate these kind of errors, because it implies that we need to add cross origin request headers to the canvas.instructure.com response from the server, which would be impossible because we don't have direct access to that web page's server.

The Solution

Luckily we discovered there is a way around this with a method available on the web page's window object called Window.postMessage(). This allowed us to communicate to content.js without receiving any errors.

Inside our 'applet', that is actually just a web page running inside an iFrame

myapplet.com
var getInfoButton = window.document.getElementById("get-started");
getInfoButton.addEventListener('click', () => {
  window.parent.postMessage('Message received!', 'https://canvas.instructure.com');
});

And in content.js, which is run inside the Chrome Extension and injected into the page:

content.js
window.addEventListener('message', (event) => {
  if (event.origin === 'https://my-lti.com') {
    alert(event.data)
  }
});

Now when we click the button from within our 'applet', an alert pop ups with "Message received!". We can now carry on and communicate from within our applet to the Chrome Extension chrome.runtime.

That is all for now folks!