Send and Receive Data Reliably in iframe using window.postMessage
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.
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?
- Primary website is loaded first, and will listen to postMessage event that will happened later
- Secondary website is loaded, and it will send
READY_TO_RECEIVE_DATA
to Primary while also listen to postMessage event that may happened
- 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
- 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 theevent.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
andkey
(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 ofiframeRef.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!