Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google OAuth2 and app script in IFRAME sandbox

I am a newbie as far as web development is concerned and even more so with Google App Scripts and OAuth2.0. Having said that, I have researched enough and also tried several tricks, but still can't get past this issue.

I borrowed sample from here:

Google Developers - Client API Library

Then created an Apps Script project with an index.html file with code from that page. I also created a project on the developer console, created a client ID, API key and turned on the required API support. I also made the required changes to the sample to reflect the new client ID and API key.

The index.html page is served from HTML Service with SandBox Mode set to IFRAME. If I load the URL in a browser window (say using incognito mode) and click "Authorize" button, it opens the Google sign-in window. But after signing in, it opens two new tabs with messages

Please close this window

and the original browser window shows no change.

The JavaScript console shows error messages like these:

Unsafe JavaScript attempt to initiate navigation for frame with URL '' from frame with URL https://accounts.google.com/o/oauth2/postmessageRelay?parent=https%3A%2F%2F…6lxdpyio6iqy-script.googleusercontent.com#rpctoken=288384029&forcesecure=1. The frame attempting navigation is sandboxed, and is therefore disallowed from navigating its ancestors.

From the messages, it seems its an effect of using IFRAME and some sort of security feature is preventing the callback being delivered to the original window. If I reload the original window, things work OK. But that's not what I would ideally like.

How do I work around this issue? Its a very simple project and I can provide source code if that helps.

Thanks, Pavan

Edit: Here is the sample code I'm trying. You would need to have your client ID and API Key and also set JS origins in the Google Console for things to work:

Code.gs

function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

index.html

<!--
  Copyright (c) 2011 Google Inc.

  Licensed under the Apache License, Version 2.0 (the "License"); you may not
  use this file except in compliance with the License. You may obtain a copy of
  the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  License for the specific language governing permissions and limitations under
  the License.

  To run this sample, replace YOUR API KEY with your application's API key.
  It can be found at https://code.google.com/apis/console/?api=plus under API Access.
  Activate the Google+ service at https://code.google.com/apis/console/ under Services
-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
  </head>
  <body>
    <!--Add a button for the user to click to initiate auth sequence -->
    <button id="authorize-button" style="visibility: hidden">Authorize</button>
    <script type="text/javascript">
      // Enter a client ID for a web application from the Google Developer Console.
      // The provided clientId will only work if the sample is run directly from
      // https://google-api-javascript-client.googlecode.com/hg/samples/authSample.html
      // In your Developer Console project, add a JavaScript origin that corresponds to the domain
      // where you will be running the script.
      var clientId = 'YOUR_CLIENT_ID';


      // Enter the API key from the Google Develoepr Console - to handle any unauthenticated
      // requests in the code.
      // The provided key works for this sample only when run from
      // https://google-api-javascript-client.googlecode.com/hg/samples/authSample.html
      // To use in your own application, replace this API key with your own.
      var apiKey = 'YOUR API KEY';


      // To enter one or more authentication scopes, refer to the documentation for the API.
      var scopes = 'https://www.googleapis.com/auth/plus.me';

      // Use a button to handle authentication the first time.
      function handleClientLoad() {
        gapi.client.setApiKey(apiKey);
        window.setTimeout(checkAuth,1);
      }

      function checkAuth() {
        gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: true, response_type: 'token'}, handleAuthResult);
      }


      function handleAuthResult(authResult) {
        var authorizeButton = document.getElementById('authorize-button');
        if (authResult && !authResult.error) {
          authorizeButton.style.visibility = 'hidden';
          makeApiCall();
        } else {
          authorizeButton.style.visibility = '';
          authorizeButton.onclick = handleAuthClick;
        }
      }

      function handleAuthClick(event) {
        gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: false, response_type: 'token'}, handleAuthResult);
        return false;
      }

      // Load the API and make an API call.  Display the results on the screen.
      function makeApiCall() {
        gapi.client.load('plus', 'v1', function() {
          var request = gapi.client.plus.people.get({
            'userId': 'me'
          });
          request.execute(function(resp) {
            var heading = document.createElement('h4');
            var image = document.createElement('img');
            image.src = resp.image.url;
            heading.appendChild(image);
            heading.appendChild(document.createTextNode(resp.displayName));
            heading.appendChild(document.createTextNode(resp.emails[0].value));

            document.getElementById('content').appendChild(heading);
          });
        });
      }
    </script>
    <script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>
    <div id="content"></div>
    <p>Retrieves your profile name using the Google Plus API.</p>
  </body>
</html>
like image 598
Pavan Deolasee Avatar asked Nov 09 '22 19:11

Pavan Deolasee


1 Answers

found a solution... not nice but works oO:

the trick is to remove the oauth2relay iframes before the auth window is closed. after the window is closed you have to add the frames again and do a immediate request, if that works the user authorized the app.

be careful:
this script does not check if the user meanwhile is logged out or the token is expired, as long as the webapp window is open the same token is used.

Code.js:

function doGet(e) {
  return HtmlService.createTemplateFromFile('Index').evaluate().setTitle(formSettings.title).setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

function include(file) {
  return HtmlService.createHtmlOutputFromFile(file).getContent();
}

function doPost(meta) {
  if (!meta || !meta.auth) {
    throw new Error('not authorized');
    return;
  }
  var auth = JSON.parse(UrlFetchApp.fetch('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + meta.auth.access_token, { muteHttpExceptions: true }).getContentText());
  if (auth.error || !auth.email) {
    throw new Error('not authorized');
    return;
  }

  if (typeof this[meta.method + '_'] == 'function') {
    return this[meta.method + '_'](auth.email, meta.data);
  }
  throw new Error('unknown method');
}

function test_(email, data) {
  return email;
}

Index.html:

<html>
  <head>
    <?!= include('JavaScript'); ?>
  </head>
  <body>
    <div class="content-wrapper">

    </div>
  </body>
</html>

Javascript.html:

<script type='text/javascript' src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type='text/javascript' src="//apis.google.com/js/client.js?onload=apiLoaded" async></script>
<script type='text/javascript'>
    var clientId = '*************-********************************.apps.googleusercontent.com';
    var scopes = ['https://www.googleapis.com/auth/plus.me', 'https://www.googleapis.com/auth/userinfo.email'];

    var loaded = false;
    var auth = null;

    function apiLoaded() {  
      loaded = true;
      login();
    }

    window._open = window.open;
    window._windows = [];
    window.open = function(url) {
      var w = window._open.apply(window,arguments);
      window._windows.push(w);
      return w;
    }

    function login(step) {
      step || (step = 0);
      if (!loaded) {
        return;
      }  
      gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: (step <= 0 || step >= 2) }, function(authResult) {
        if (authResult) {
          if (authResult.error) {
            if (authResult.error == 'immediate_failed' && authResult.error_subtype == 'access_denied' && step <= 0) {
              var interval = setInterval(function() {
                var $ifr = $('iframe');//[id^=oauth2relay]');
                if (!window._windows.length) {
                  clearInterval(interval);
                  return;
                }
                if ($ifr.length) {
                  clearInterval(interval);
                  $ifr.detach();
                  var w = window._windows.pop();
                  if (w) {
                    var interval2 = setInterval(function() {
                      if (w.closed) {
                        clearInterval(interval2);
                        $('body').append($ifr);
                        login(2);
                      }
                    });
                  } else {                
                    $('body').append($ifr);
                  }
                }
              },500);
              login(1);
            } else if (authResult.error == 'immediate_failed' && authResult.error_subtype == 'access_denied' && step >= 2) {
              //user canceled auth
            } else {
              //error
            }
          } else {
            auth = authResult;
            doPost('test', { some: 'data' }, 'test');
          }
        } else {
          //error
        }
      });
    }

    function test() {
      console.log(arguments);
    }

    //with this method you can do a post request to webapp server
    function doPost(method, data, callbackName) {
      data || (data = {});
      google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onError).withUserObject({ callback: callbackName }).doPost({ method: method, data: data, auth: auth });
    }

    function onSuccess(data, meta) {
      if (typeof window[meta.callback] == 'function') {
        window[meta.callback](null, data);
      }
    }

    function onError(err, meta) {
      if (typeof window[meta.callback] == 'function') {
        window[meta.callback](err);
      }
    }
</script>
like image 156
AntiCampeR Avatar answered Nov 14 '22 23:11

AntiCampeR