Search code examples
javascripthtmliframepostmessage

How to create dynamic iframe that works with postMessage


I am working on a project where our web app needs to integrate with a 3rd party app via the postMessage protocol. For illustrative purposes, say this is the code, which works just fine:

<html>

<head>

<script>
var ovsDomain = '3rd.party.vendor',
    ovsOrigin = 'https://' + ovsDomain,
    ovsApiUrl = ovsHost + '/webapi.html';

function begin() {
    var ovsFrame = document.getElementById( 'ovs' );

    if ( window.postMessage ) {
        if ( window.addEventListener ) {
            window.addEventListener( 'message', onMessage );
        } else {
            window.attachEvent( 'onmessage', onMessage );
        }
    }

    ovsFrame.src = ovsApiUrl + '#' + encodeURIComponent( document.location.href );
    ovsFrame.contentWindow.postMessage( JSON.stringify( {
        cmd:        'initialize',
        session:    'test_session'
    } ), ovsOrigin );
}

function post() {
    var ovsFrame = document.getElementById( 'ovs' ),
        msg = JSON.stringify( {
            cmd:    'send',
            data:   'test data'
        } );

    ovsFrame.contentWindow.postMessage( msg, ovsHost );
}

function end() {
    var ovsFrame = document.getElementById( 'ovs' );

    ovsFrame.contentWindow.postMessage( JSON.stringify( {
        cmd: 'close'
    } ), ovsOrigin );
}

function onMessage( evt ) {
    var data,
        ovsFrame = document.getElementById( 'ovs' );

    if ( evt.origin === ovsHost && ovsFrame.contentWindow === evt.source ) {

        try {
           data = JSON.parse(evt.data);
        } catch ( e ) {
           alert( 'bad data' );
        }

        switch ( data.msg ) {
        case 'close':
            alert( 'closed' );
            break;
        case 'initialized':
            alert( 'initialized' );
            break;
        }
   }
}

</script>

</head>

<body>
<input type='button' value='Begin' onclick='begin()' />
<input type='button' value='Post' onclick='post()' />
<input type='button' value='End' onclick='end()' />
<iframe id='ovs' sandbox='allow-scripts allow-same-origin allow-popups' src='https://3rd.party.vendor/webapi.html' style='display:none'></iframe>
</body>

</html>

I want to conditionally create/load the iframe element based on a) whether the feature is enabled and b) whatever the URL should be for the particular client and/or vendor API we're using is. So I tried removing the static iframe from the HTML document and adding the following tweaks to my begin function:

function begin() {
    var ovsFrame = document.createElement( 'iframe' );

    if ( window.postMessage ) {
        if ( window.addEventListener ) {
            window.addEventListener( 'message', onMessage );
            ovsFrame.addEventListener( 'load', onFrameLoad );
        } else {
            window.attachEvent( 'onmessage', onMessage );
            ovsFrame.attachEvent( 'load', onFrameLoad );
        }
    }

    ovsFrame.id = 'crs';
    ovsFrame.src = ovsOrigin;
    document.body.appendChild( ovsFrame );
}

function onFrameLoad() {
    var ovsFrame = document.getElementById( 'ovs' );

    ovsFrame.src = ovsApiUrl + '#' + encodeURIComponent( document.location.href );
    ovsFrame.contentWindow.postMessage( JSON.stringify( {
        cmd:        'initialize',
        session:    'test_session'
    } ), ovsOrigin );
}

the iframe is created and added to the DOM. the initial URL is loaded, but a popup the API wants to create is blocked. more importantly, my attempt to postMessage is blocked:

Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://3rd.party.vendor') does not match the recipient window's origin ('https://althost.meditech.com:4433').

my understanding is that because I created the iframe in my document, it inherits my document's domain. and once that is done, I can't force-set it to the OV's domain, e.g.

ovsFrame.contentWindow.document.domain = ovsDomain;

due to the fact that the OV domain is unrelated to mine.

for all the documentation I can find, nowhere is it explicitly stated that you cannot do this (create a dynamic element) though nowhere have I found an example where someone hasn't had a static iframe element in the HTML so it's starting to feel like one of those dirty little secrets (or conventional wisdom tidbits) that I am not privy to.

any advice would be appreciated, even if it's confirmation that it can't be done. I've read through the Same-origin policy documentation and


Solution

  • I have determined a way to get around this issue, in case anyone else experiences it and needs a resolution. Creating the iframe element dynamically will inherit the script page's origin; since it's my page, I can load an HTML document that I am hosting, which will statically load the 3rd party iframe. Once I have this new page in the mix, I can use window.postMessage to communicate through the new page to the 3rd party page, in effect proxying the communication through my trusted origin. It feels a little clunky, but it seems sound and works.