Send and Receive Data Reliably in iframe using window.postMessage

 
Courtesy:
Courtesy: stablediffusionweb.com, prompt: “two web browser window that communicate each other, not a character”
 

Hi, at this occasion I’m gonna tell you something that I learn in recent project at my company.

So the situation is currently we have a website that being used as a webview in Android app. Let’s call this webview site: Secondary website and the domain is secondary.com.

But now we want to embed that website too in our another website as a iframe. Let’s call this main website: Primary website and the domain is primary.com.

 

 

If you don’t know what iframe is, it’s basically a HTML tag that we can use to display another website into our site. The common example is Youtube, we can embed the Youtube video player into our website.

notion image

If you right click at any Youtube video, and click “Copy embed code”, it will copy the complete iframe tag that we can paste into our site code to display that Youtube video into our website.

With that we don’t need to create our own video player anymore, because we use the video player that Youtube have.

 

You can learn more about iframe here:

 

And that’s what we gonna today, we want to embed our Secondary website so we don’t need to develop that again into our Primary website.

 

 

As you can see, displaying it is easy, just use <iframe src=’secondary.com’ />, then our job is done.

But what if we want to send any data from the Primary to Secondary? And what if one of the data is token that needed for authentication inside the Secondary website, which can be expired so should be supplied periodically? Then, it’s not enough.

 

What our options here?

  • Using search query, like: <iframe src='secondary.com?search=book' />
    • pros: easy to implement, cross-origin
    • cons: static
  • Using postMessage, it’s gonna be window.postMessage({ search: 'book' }, ‘*’)
    • pros: dynamic, cross-origin
    • cons: more line of code
  • Using localStorage, use it normally localStorage.setItem('search', 'book')
    • pros: easy to implement
    • cons: same-origin, static
  • … (actually there are another options, but it’s should same origin)
 

But wait, what same-origin and cross-origin mean? same-origin mean the protocol, full domain, and the port is same. Let’s take a look the examples:

  • Not same origin: domain.com and sub.domain.com, because they have different full domain (although sub.domain.com is subdomain of domain.com)
  • Not same origin: http://www.website and https://www.website.com, because they have different protocol (http vs https)
  • Same origin: https://ilhamwahabi.com:3000 and https://ilhamwahabi.com:3000, because the protocol, full domain, and the port is same
 

Then our options left to search query and postMessage, because our website is located in primary.com and secondary.com so it’s cross-origin.

Now it depends on our use case.

If we just want to send simple data, not sensitive, and static then we can just use search query.

But in this case since our Secondary website want to receive token for authentication then we can’t use search query because it’s sensitive and need to updated dynamically later, that’s why our choice left to postMessage.

With postMessage we can send data without exposed it, and when Primary website want to supply new token into Secondary website we can call the function again. Dynamic!

Also If we use search query and our Secondary website has routing, then the data that it receive in the URL at the beginning will be disappeared when it’s changing pages. Very cumbersome.

 

 

Now we understand the use case and why we choose it, then how to use it?

Since my company project use React for this project, I will also give example in React.

// Primary Website, primary.com
import { useRef, useEffect } from 'react'

const Primary = (props) => {
	const iframeRef = useRef()
	const { search } = props
	
	useEffect(() => {
		const sendDataToSecondary = (event) => {
			const { data, origin } = event
		
			// Prevent to receive data from another origin
			if (origin !== 'https://secondary.com') return
		
			if (data.title === 'READY_TO_RECEIVE_DATA') {
				iframeRef.current?.contentWindow.postMessage(
					{ title: 'SEND_DATA', search },
					'https://secondary.com', // Only Secondary can receive this
				)
			}
		}
	
		window.addEventListener('message', sendDataToSecondary)
		
		return () => window.removeEventListener('message', sendDataToSecondary)
	}, [])
	
	return (
		<iframe ref={iframeRef} src='https://secondary.com' />
	)
}

export default Primary
// Secondary Website, secondary.com
import { useState, useEffect } from 'react'

const Secondary = () => {
	const [search, setSearch] = useState('')
	
	useEffect(() => {
		// Notify Primary that iframe is loaded and ready to receive data
		window.postMessage(
			{ title: 'READY_TO_RECEIVE_DATA' },
			'https://primary.com', // Only Primary can receive this
		)
		
		const receiveDataFromPrimary = (event) => {
			const { data, origin } = event
		
			// Prevent to receive data from another origin
			if (origin !== 'https://primary.com') return
			
			if (data.title === 'SEND_DATA') {
				setSearch(data.search)
			}
		}
		
		// Listen when data received from Primary
		window.addEventListener('message', receiveDataFromPrimary)

		// Remove the listener when unmount
		return () => window.removeEventListener('message', receiveDataFromPrimary)
	}, [])
	
	return (
		<p>{search}</p>
	)
}

export default Secondary

How two web app above works?

  1. Primary website is loaded first, and will listen to postMessage event that will happened later
  1. Secondary website is loaded, and it will send READY_TO_RECEIVE_DATA to Primary while also listen to postMessage event that may happened
  1. Primary website got the ‘message’ event, check the origin and passed, then check the title and based on the title Primary will send SEND_DATA to Secondary with search data
  1. Secondary website got the ‘message’ event, check the origin and passed, then check the title and based on the title Secondary will save the data that being send and display it
 

Q: But, you said before that in our use case we want to send auth token?

A: Yeah, I just want to keep the example simple to help you get the big idea.

But, this is my idea how to implement the token system:
  • in Primary we can have a watcher or setInterval that notify the Secondary when the token refreshed and send that newly generated token, something like: {title: ‘SEND_TOKEN’, token: 'ADfasdfhlidfsahllhh3l23lhr'}
  • In Secondary, we watch for SEND_TOKEN event and store the event.token to access it elsewhere for authentication
 

Now after you take a look at above implementation, there are some things that you should notice

  • In Primary website we received props, if the props changed the page will be re-render, so is the iframe gonna be re-rendered/reloaded too?
    • No, iframe only re-rendered when src and key (if any) changed
  • Why Secondary need to send the event first?
    • Because depends on the site it might take time to load, so it will be reliable if we wait it loaded first then send event to Primary to notify that it’s ready to receive event
  • Instead of sending data, can we just send function instead?
    • No, I found we can’t do that
  • In Primary, why we set listener to window instead of iframeRef.current.contentWindow?
    • Because the ‘message’ event is part of the global window object
  • Why you not handle when iframeRef.current is null?
    • In above case no need to do that, since iframeRef.current is accessed inside listener event, and that listener event only being called when we receive data from the iframe (so the iframeRef should not be null anymore)
    • I use optional chaining (?.) to prevent Typescript rage
  • Is origin checking required?
    • No, but we already know that postMessage can be use to communicate cross-origin, so we should be able to prevent malicious website communicate with our website to ensure that we only receive data from allowed origin
  • Is the receiver URL as second parameter in window.postMessage required?
    • Yes, it’s to restrict to whom we will send the data
    • You can use * if you want to bypass this anyway, that mean any origin can receive that event, for example if you already set checking elsewhere
 

You can learn more about postMessage here:

 

 

I think that’s all guys. I hope you learn something new today. If you know better alternatives that we can use, please let me know.

And don’t forget to share this if you think this post is helpful. Cheers!