krakenjs / zoid

Cross domain components
Apache License 2.0
2.03k stars 248 forks source link

Error: Parent component window is on a different domain #67

Open jmc265 opened 7 years ago

jmc265 commented 7 years ago

I have 2 xcomponents and 3 sites:

http://localhost:8008 (button.js xcomponent) http://localhost:8004 (pay.js xcomponent) http://localhost:8005 (uses button.js)

So site 8005 imports button.js from 8008. When a user clicks on the button, the script uses pay.js to open a popup window with 8004 site in. Very similar to how PayPal works (user clicks the PayNow button and the checkout appears in popup/lightbox).

So in the 8008 site, I have code like:

    document.querySelector('#payButton').addEventListener('click', function(event) {
        let props = {
            ...
        };

        //Pay.renderPopup(props);
       Pay.renderPopupTo(window.parent, props);
    });

When renderPopupTo() is called, the lightbox is created over the parent window, the popup window is opened and the location for the popup is correctly http://localhost:8004. However, in the popup window I get the error:

Error: Parent component window is on a different domain - expected http://localhost:8004 - can not retrieve props at ChildComponent.value

If I just call renderPopup() instead, everything works fine EXCEPT the lightbox is only shown over the button, not over the whole parent window.

So, what is wrong with my usage of renderPopupTo()?

jmc265 commented 7 years ago

I think I have to host button.js and pay.js on the same domain?

bluepnume commented 7 years ago

Yeah you're right on the money -- gotta run right now, but will give a more detailed answer soon. Short version is, there's a technical restriction which means if you want to to renderTo(), the frame/window being rendered and the frame/window doing the rendering need to be on the same domain. The parent page can be a different domain though.

bluepnume commented 7 years ago

So, the reason for this is, for renderTo() we have to pass the props over to the other window securely.

For same-page rendering that's easy, we can just pass the props directly.

For rendering to a remote page with renderTo(), we'd have to pass the props through an untrusted domain. That may be ok in certain scenarios, but didn't want to enable it by default in xcomponent. So, instead, we restrict the child component to the same domain as the renderer, that way the props can be passed securely using a cross-window global variable.

Depending on demand, could probably add some kind of trusted domain setting to allow passing props through the parent window.

But for now, yep, button.js and pay.js have to be the same domain.

------------------------------
|    parent page [ x.com ]   |
|                            |
|     --------------------   |
|     | button [ y.com ] |   |
|     ----------|---------   |
|               |            |
|     renderTo(win.parent)   |
|               |            |
|     ----------v---------   |
|     |   pay [ y.com ]  |   |
|     --------------------   |
|                            |
------------------------------

Work for you?

ghost commented 7 years ago

Just a side notes... I confirmed that the approach @bluepnume suggested works fine as long as you configures remoteRenderDomain correctly in the pay.js.

Ex:

window.MyLightbox = xcomponent.create({

    // The html tag used to render my component
    tag: 'my-lightbox-component',

    // The url that will be loaded in the iframe or popup, when someone includes my component on their page
    url: 'https://y.com/mylightbox',

    context: 'lightbox',

    // The size of the component on their page
    dimensions: {
        width: 350,
        height: 450
    },

    remoteRenderDomain: /.*/,

    // The properties they can (or must) pass down to my component
    props: {

        onComplete: {
            type: 'function'
        }
    }
});
jmc265 commented 7 years ago

@bluepnume Thanks for the explanation, that makes perfect sense. It would be good to optionally allow this behaviour in my opinion. In the case of the pay button and checkout, we would just be echoing the parameters the parent gave us anyway.

But thanks for this excellent project, it looks to be very nicely put together!

bluepnume commented 7 years ago

@kms-ky-truong that's correct -- although with the latest version I actually removed the requirement forremoteRenderDomain.

@jmc265 yeah, there's no technical reason why not to, only a security one. I want to make sure people don't accidentally pass down secure/sensitive props, and don't realize another domain that they don't own is getting access to them. But maybe this can be solved with an additional option to white-list the intermediary domain as "safe". It would certainly make renderTo more powerful

bluepnume commented 7 years ago

@jmc265 / @kms-ky-truong wanted to get your thoughts. I had a little bit of a think this weekend about how to explicitly set up trusted domains, to allow a universal renderTo without requiring that the rendering component necessarily be on the same domain as the rendered component.

The reason I built renderTo in the first place was to allow a child component to "render-up" to the parent page, so PayPal buttons could "render-up" the checkout flow from the button to the merchant page. There are a few more details about that use-case in this article.

For that use-case, it works because:

So the Button can pass props through to Checkout without them being intercepted by the potentially untrusted intermediate page, by virtue of them being on the same domain.


What I'm trying to figure out is, what are some examples of use-cases for xcomponent where the three windows would all be on different domains?

Definitely in a dev env, where you're testing with different servers running on different ports. But are there any production use-cases you guys can think of?

The reason I ask is, I want to create a nice way to specify this trust. Right now I'm thinking something like:

let MyComponent = xcomponent.create({

    // Allow remote render from these domains
    trustedDomains: [ 'http://x.com', 'http://y.com' ]
});
MyComponent.renderTo(win, {

    // Allow these domains to pass-along props
    trustedDomains: [ 'http://a.com', 'http://b.com' ]
});

But understanding some solid use-cases would be useful before defining this trust interface.

ghost commented 7 years ago

@bluepnume : I don't find any use case where the three windows are on three different domains. However, I have to say that I couldn't agree more with you. You're providing another "secure utility" that I would love to use.

bluepnume commented 7 years ago

So far, I'm following the same rules that browsers follow for cross-domain security. So a page is only considered 'same domain' so long as:

I want to make sure people aren't taken by surprise with any of these rules being more permissive than browser-defaults. So, with these restrictions, allowing subdomains by default would be a no-go.

That said, it's totally possible to have any domain setting xcomponent offers to accept an array or a regex, to allow multiple domains or a domain patterns. That way, people could explicitly allow whichever domains or sub-domains they like, for instance /^https:\/\/.*\.foo\.com$/ to allow any subdomain of foo.com.

ghost commented 7 years ago

It's perfect with regex. Big thump up 👍

jmc265 commented 7 years ago

So I don't currently have a production example of the different components being on different domains, but in a micro-service world I could definitely see how they could be. If I have a checkout front-end which is already in place, I might host that at https://checkout.domain.com. Now, I want to offer Buttons to 3rd party merchants and I don't want to tie my new xcomponent to my checkout service. So I would host my Button xcomponent at https://button.domain.com/component.js. The checkout flow would then have to have its own xcomponent at https://checkout.domain.com/component.js. Which would mean that my Button component would need to whitelist my Checkout domain to allow for sending of information to it.

I am perhaps not understanding the technical mechanism behind the scenes here but: why is there a problem with security here? If I want to send information securely from button component to checkout component, can't the button component do that directly via postMessage?

bluepnume commented 7 years ago

OK, that's a pretty good example. Thanks!

I am perhaps not understanding the technical mechanism behind the scenes here but: why is there a problem with security here? If I want to send information securely from button component to checkout component, can't the button component do that directly via postMessage?

You're totally right -- the problem is, the way xcomponent is designed, the initial props are passed down synchronously through the window.name of the frame or popup.

The reason for that is, it enables you to start using window.xprops immediately, without always worrying about doing something like window.xchild.waitForProps().then(props => { ... }) every time you want to safely use the props that were passed down. Otherwise accessing props without waiting for them at any point ends up being a race condition, which would probably be intermittent and very tricky to debug.

So, in the renderTo() iframe case, the iframe is created by a third party, potentially untrusted window -- but it still needs a window name to set on the iframe, which contains props that it's potentially not privileged to access. In this case, xcomponent doesn't pass the props themselves, just a reference id, then when the child window loads it synchronously reaches into a global on the original window with that id. That relies on the renderer and the renderee being on the same domain, to access that global.

So adding trustedDomains here would allow us to say "It's ok to pass the props to via this intermediary window" so it can use them to construct the initial window name.

That said, now I'm thinking about it -- none of this helps if you actually want to render through an intermediary domain which isn't trusted by you. Like in your button example, we wouldn't necessarily want to expose the props from the button to the parent window. So this may take some more thought.

I could just open up an asynchronous window.xprops, and rely on post-messaging, but then we end up with two different interfaces for getting props, and the same potential race-condition from before.

Probably need to mull this over some more. Open to suggestions though.

rickyH commented 3 years ago

We have exactly the same issue and end up string replacing the check in the bundle to allow subdomains. It's a hack we wish we didn't have to do.

Essentially the iframe initially points to: account.x.com/authorize (Which is IDS4) This redirects to authentication x.com/sign-in (This page hosts the ZOID scripts) Once authentication is completed we redirect to account.x.com/authorize account.x.com/authorize

The only sensible change we can think of is to host a page that does the following: x.com/start (redirects to account.x.com/authorize which redirects back to x.com/sign-in)

Unless of course we could have an allowed list of trusted domains. Which suits our needs perfectly.