dozoisch / react-google-recaptcha

Component wrapper for Google reCAPTCHA
MIT License
1.04k stars 145 forks source link

ReCaptcha in iFrame not working #65

Open Manuel-Manoury opened 6 years ago

Manuel-Manoury commented 6 years ago

Hi !

I'm trying to use your component in an iframe but it fails with a "cannot join the ReCaptcha service" message (without any call to the verify callback). I guess it's an issue due to some script included / operations done at the window level, without the ability to specify a custom window context (as the component works just fine if I replace the iframe by a div). I think it could be interesting to add the behavior to your component to make it more versatile. If you do not think so, do you have an idea, a workaround or something to make your component work in an iframe to help me solve my corner-case ?

Thanks.

dozoisch commented 6 years ago

Can you show an example of how you use it inside an iframe? But it seems like the google script would need to be loaded inside the iframe too.

Manuel-Manoury commented 6 years ago

Hi !

Here are the informations I find relevant:

Context

I'm working on a widget, a bit like Intercom, which is intended to be loaded on a client website through very few operations. In order to avoid messing up the client CSS, I'm setting up my widget in an iframe through the use of react-frame-component. I do have some amount of control on what's present in the rendered iframe through the props head and initialContent (which I already use to embed inline CSS to avoid the client having to include the CSS file by himself).

Application structure

I got rid of quite a lot of code for readability, but here is the idea:

import React from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import WidgetFrame from 'react-frame-component';
import { ConnectedRouter } from 'react-router-redux';
import { Route } from 'react-router';
import { Provider } from 'react-redux';

const store = createStore(...);

const SetupView extends React.Component {
  constructor (props) {
   super(props);
   this.handleReCaptchaChanged = () => {
       // stuff
   };
  } 

  render () {
    return (
        <div>
          { ... }
          <ReCAPTCHA
            sitekey={secureKey.key}
            onChange={this.handleReCaptchaChanged}
          />
        </div>
    );
}
}

const Layout = ({ children }) => {
// __IFRAME_CONTENT__ is replaced during the browserify build operation to match a one-line version of a HTML template, which embed custom iframe-scoped style
    return (
       <WidgetFrame
           initialContent='__IFRAME_CONTENT__'
           id='app-iframe'
        >
          { ... }
          {children}
    </WidgetFrame>
    );
};

const Router = ({ history }) => {
    return (
        <ConnectedRouter history={history}>
          <Layout>
            <Route exact path='/' component={...} />
            { ... }
            <Route path='/setup' component={SetupView} />
          </Layout>
        </ConnectedRouter>
    );
};

// `app` is the button my sdk creates and mounts into the client's webpage
ReactDOM.render(
  <Provider store={store}>
    <Router history={history} />
  </Provider>,
  app
);

Fix tried

I did try a few things in order to fix the recaptcha, such as copying (during runtime, after the Layout component is mounted to the DOM) the recaptcha script tags that are generated into the main window.head into the iframe head, without any noticeable changes (it was a naive and desperate attempt, I'm pretty sure there's much more going on than just setting up a <script> tag)

Do not hesitate if I can provide you with more specific informations.

Thanks for your help :)

Manuel-Manoury commented 6 years ago

Hi ! did you try to reproduce the issue described above ?

dozoisch commented 6 years ago

Hey @Manuel-Manoury I haven't had time yet. I just checked the lib you are using for iFrame. I suspect that there would need to be changes made to react-async-script.

The IFrame lib passes you the window from the context like

const MyComponent = (props, context) => {
  const {
    document: iframeDocument,
    window: iframeWindow
  } = context;

  return (<...rendered jsx.../>);
};

MyComponent.contextTypes = {
  window: PropTypes.any,
  document: PropTypes.any
};

and so react-async-script would need to be able to use that context. Probably by having the window/document passed as a prop. And it would then use that instead of the global window/document.

Manuel-Manoury commented 6 years ago

I looked for solutions today too, and I came to the same conclusion. I intend to look more into react-async-script, but what feels weird is that this lib is intended for google maps and such, that would be kinda weird for it not to be useable inside an iFrame... Anyway, I'll look further into this issue and I will keep you updated :) Thanks for your help :D

jamealg commented 6 years ago

@Manuel-Manoury Were you ever able to find a solution to this problem?

Manuel-Manoury commented 6 years ago

@jamealg I did actually, it's quite dirty but I forked this repo (sorry it's in a private organization - as I do not really have time to maintain it, at least for now) and I modified the code just a little bit to add context to the constructor, and create a this._window = context.window || window; that is reused instead of "simply" using the window object.

Please do note that I'm only using explicit rendering, thus I might not have all the answers to your use case.

It's a bit messy but I would advise you to :

  1. pay attention to browser compatibility : as of now, on Edge I have an issue that makes the recaptcha unusable. I guess it has something to do with the custom iframe settings such as the referrer policy and such (btw, if you face and fix this issue, let me know :D ). Also, I recently faced issues on Firefox, which needs the custom iframe src to be set to the iframe hosting window.location in order to work (but should be unspecified on chrome otherwise it would cause some encryption issue and a timeout). Well, you get the idea, we end up with an iframe (the recaptcha one) in an iframe (your custom iframe), which can be tricky to handle correctly
  2. make sure to load the recaptcha script in your custom iframe, to have grecaptcha available in the context
  3. try weird stuff to get it working : I honestly don't remember exactly why, but I had to postpone the rendering of the recaptcha (using a wrapper component, to take advantage of the componentDidMount lifecycle hook)

These reason are also a part of why it's a private fork : I think quite poorly of this codebase, there are too many hacks and tricks involved in my opinion, but I do not have the bandwidth to tackle this for now.

Hoping it might help you, feel free to keep me updated if you happen to find a cleaner way to do this.

Cheers :)

jamealg commented 6 years ago

Thank you @Manuel-Manoury I appreciate the insight. I'll definitely have to figure out that Edge issue so hopefully it's not too troublesome. I'll follow up if I come up with anything good!