Google picker auth popup is being blocked

2020-02-28 04:46发布

问题:

I have a mobile site which lists jobs, the user applies and uploads their CV (resume) - I want them to be able to choose a file from their Google Drive.

I've created the Hello world example here - https://developers.google.com/picker/docs/ (code reproduced here for convenience)

Problem is that if not already logged into Drive, a popup to login is launched. This is bad enough on a desktop but really bad on a phone.

I have tried this solution, but get 'TypeError: gapi.auth is undefined'

I also tried launching the picker from an onclick event rather than the onload as described by the docs.

function launchDrive()
    {
        gapi.load('auth', {'callback': onAuthApiLoad});
        gapi.load('picker', {'callback': onPickerApiLoad});
    }
<input type='button' value='Launch Drive' onclick='launchDrive();'>

Sample Google code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>Google Picker Example</title>

    <script type="text/javascript">

      var developerKey = 'xxxxxxxYYYYYYYY-12345678';
      var clientId = "1234567890-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com"
      var scope = ['https://www.googleapis.com/auth/photos'];

      var pickerApiLoaded = false;
      var oauthToken;

      function onApiLoad() {
        gapi.load('auth', {'callback': onAuthApiLoad});
        gapi.load('picker', {'callback': onPickerApiLoad});
      }

      function onAuthApiLoad() {
        window.gapi.auth.authorize(
            {
              'client_id': clientId,
              'scope': scope,
              'immediate': false
            },
            handleAuthResult);
      }

      function onPickerApiLoad() {
        pickerApiLoaded = true;
        createPicker();
      }

      function handleAuthResult(authResult) {
        if (authResult && !authResult.error) {
          oauthToken = authResult.access_token;
          createPicker();
        }
      }

      // Create and render a Picker object for picking user Photos.
      function createPicker() {
        if (pickerApiLoaded && oauthToken) {
          var picker = new google.picker.PickerBuilder().
              addView(google.picker.ViewId.PHOTOS).
              setOAuthToken(oauthToken).
              setDeveloperKey(developerKey).
              setCallback(pickerCallback).
              build();
          picker.setVisible(true);
        }
      }

      // A simple callback implementation.
      function pickerCallback(data) {
        var url = 'nothing';
        if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
          var doc = data[google.picker.Response.DOCUMENTS][0];
          url = doc[google.picker.Document.URL];
        }
        var message = 'You picked: ' + url;
        document.getElementById('result').innerHTML = message;
      }
    </script>
  </head>
  <body>
    <div id="result"></div>

    <!-- The Google API Loader script. -->
    <script type="text/javascript" src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
  </body>
</html>

13 May 2015 edit

Further to Jason's answer, here is what I also tried (called by a button oncllick):

function launchDrive()
    {
        //gapi.load('auth', {'callback': onAuthApiLoad});
        gapi.auth.init(onAuthApiLoad);
        gapi.load('picker', {'callback': onPickerApiLoad});
    }

回答1:

To solve your issue you need to understand how google performs oauth in your case:

  • gapi performs init actions
  • gapi opens a google auth page in new popup window and you perform login in it
  • after login succeeded gapi gets notified and you receive your token

Why browser blocks the popup in 2nd step:

  • original event is not present anymore in window (window.event is destroyed).
  • User manually blocked the popup from current site

So if user didn't block a popup and you popup is still blocked, gapi actions look something like:

<input type="button" onclick="auth" value="click"/>

function auth() {
  setTimeout(function() {
     // by this time window.event is destroyed, that's why browser blocks the popup
     window.open(document.URL, '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
  }, 100)
}

So what you should do:

  • Make sure that after button click you don't perform any asynchronous actions like XHRequests or other
  • Make sure gapi is inited and ready by the time users click on the button, so by the time gapi needs to create a popup, window.event won't be null. So move all gapi init methods to DOMContentLoaded.
  • As another option you could use server-side oauth flow, which means instead of popup user will be redirected in current tab to gauth page.


回答2:

You will want to call gapi.auth.init. See the docs here: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauthinit

Initializes the authorization feature. Call this when the client loads to prevent popup blockers from blocking the auth window on gapi.auth.authorize calls.



回答3:

I have it working now.

In the example for the picker, https://developers.google.com/picker/docs/, it calls:

 <script type="text/javascript" src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>

In this example, https://developers.google.com/api-client-library/javascript/start/start-js, it calls:

<script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>

Using client.js fixes the 'TypeError: gapi.auth is undefined' issue, and thus the login popup works.

Maybe api.js is an older version of the API?



回答4:

Just Skip to the bottom

Here's the code that works for me currently. This is my first hour using this API though, so I really don't know what any of these functions do yet, nor do I know what the proper order and error handling is just yet, but at least this is functional now. Maybe it'll help someone else in the future.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>Google Picker Example</title>
</head>
<body style="width: 70%; margin: 100px auto;">

    <!-- Added a button to open picker -->
    <button onclick="loadPicker();" >Open from GoogleDrive</button>
    <div id="result"></div>

    <!-- Moved to end of body tag instead of head -->
    <script type="text/javascript">

    // The Browser API key obtained from the Google API Console.
    // Replace with your own Browser API key, or your own key.
    var developerKey = '<IDK WHAT'S SUPPOSED TO GO HERE, BUT ITS OK>';

    // The Client ID obtained from the Google API Console. Replace with your own Client ID.
    var clientId = "<YOUR CLIENT ID GOES HERE>.apps.googleusercontent.com"

    // Replace with your own project number from console.developers.google.com.
    // See "Project number" under "IAM & Admin" > "Settings"
    var appId = "<YOUR APP ID GOES HERE>";

    // Scope to use to access user's Drive items.
    var scope = ['https://www.googleapis.com/auth/drive'];

    var pickerApiLoaded = false;
    var oauthToken;

    // Use the Google API Loader script to load the google.picker script.
    function loadPicker() {
        // This needs to be client:auth2 no client
        gapi.load('client:auth2', {'callback': onAuthApiLoad});
        gapi.load('picker', {'callback': onPickerApiLoad});
    }

    function onAuthApiLoad() {
        // we need to init gapi.client with the clientId and scope first
        gapi.client.init({
            clientId: clientId,
            scope: scope
        });

        // Now we can authorize? seems like the same thing here
        window.gapi.auth.authorize(
        {
        'client_id': clientId,
        'scope': scope,
        'immediate': false
        },
        handleAuthResult);
    }

    function onPickerApiLoad() {
        pickerApiLoaded = true;
        createPicker();
    }

    function handleAuthResult(authResult) {
        if (authResult && !authResult.error) {
            oauthToken = authResult.access_token;
            createPicker();
        }
    }

    // Create and render a Picker object for searching images.
    function createPicker() {
        // Wow this is a mess
        if (pickerApiLoaded && oauthToken) {
            var view = new google.picker.View(google.picker.ViewId.DOCS);
            view.setMimeTypes("image/png,image/jpeg,image/jpg");
            var picker = new google.picker.PickerBuilder()
            .enableFeature(google.picker.Feature.NAV_HIDDEN)
            .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
            .setAppId(appId)
            .setOAuthToken(oauthToken)
            .addView(view)
            .addView(new google.picker.DocsUploadView())
            // Guess this is... optional?
            //.setDeveloperKey(developerKey)
            .setCallback(pickerCallback)
            .build();
            picker.setVisible(true);
        }
    }

    // A simple callback implementation.
    function pickerCallback(data) {
        if (data.action == google.picker.Action.PICKED) {
            var fileId = data.docs[0].id;
            alert('Selected fileId: ' + fileId);
        }
    }
    </script>

    <!-- The Google API Loader script. Removed the autorun -->
    <script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
</body>
</html>

Edit: If you get a pop-up window that doesn't load, just close it and click the button again. That fixed another issue I just had.

Again, I don't know what I'm doing yet, so hopefully I can get a better understanding of this and clarify things later.

E2: Ah, there's more information about OAuth2 over on the Javascript GAPI documentation page which can be found here: https://developers.google.com/api-client-library/javascript/features/authentication

From another document, it appear that gapi.load('client', callback) will load auth2 if not already loaded. Calling gapi.load('client:auth2', callback) will just save 1 network request.

Note: when you authorize your application using Oauth 2.0, you do not also need to set the API key as in the first example. However, it is a good practice to do so, in case your code ever expands to handle unauthorized requests.

That explains why I could remove the API/developer key.

Edit 3: Ok the above code is technically wrong.

Warning: do not use this method alongside the recommended gapi.auth2.init and signIn flow. These are two distinct behaviors (Authorization for gapi.auth2.authorize vs Authentication for gapi.auth2.init/signIn) and will have unexpected issues if used within the same application.

autorize is for single use authentications (if you were logged into 2 google accounts for instance). While using gapi.init() is meant to be for a more long term session (like for logging in and out of a website).

How this is working currently, I do not know.


Don't use the above code, just wanted to document the progress. Here's a better demo working with getAuthResponse()

<html>
  <head></head>
  <body>
    <div style="padding: 50px">
        <h2 style="color: #2196f3;">Status: <span id='status'></span></h2>
        <button id="signin-button" onclick="handleSignInClick()">Sign In</button>
        <button id="signout-button" onclick="handleSignOutClick()">Sign Out</button>
        <button id="signout-button" onclick="openFile()">Open File</button>
    </div>

    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript">
    var cid = '<CLIENTID_HERE>';
    var scope = 'https://www.googleapis.com/auth/drive';
    var authenticated = false;
    var pickerLoaded = false;
    var auth = null;
    var user = null;
    var response = null;
    var token = null;
    var stat = $('#status');

      function openFile() {
        gapi.load('client:auth2', initClient);
        gapi.load('picker', onPickerLoad);
      }

        function initClient() {
            stat.html("starting");

            gapi.client.init({
                clientId: cid,
                scope: scope
            }).then(
            function () {
                console.log("init");

                // Check if we are logged in.
                auth = gapi.auth2.getAuthInstance();
                auth.isSignedIn.listen(onStatusChange);
                authenticated = auth.isSignedIn.get();
                stat.html(authenticated);

                if (authenticated) {
                    stat.html("Logged In!");
                    user = auth.currentUser.get();
                    response = user.getAuthResponse(true);
                    token = response.access_token;
                    showPicker();
                } else {
                    stat.html("Logged Out!");
                }
            }, function(){stat.html("error")});
        }

        function onStatusChange(isSignedIn) {
            if (isSignedIn) {
                stat.html("Logged In!");
                authenticated = true;
                user = auth.currentUser.get();
                response = user.getAuthResponse(true);
                token = response.access_token;
                showPicker();
                showPicker();
            } else {
                authenticated = false;
                stat.html("Logged Out!");

            }
        }

        function handleSignInClick(event) {
            gapi.auth2.getAuthInstance().signIn();
        }

        function handleSignOutClick(event) {
            gapi.auth2.getAuthInstance().signOut();
            alert("signed out");
        }


        function onPickerLoad() {
            pickerLoaded = true;
            showPicker();
        }

        function showPicker() {
            if (pickerLoaded && authenticated) {
                var view = new google.picker.View(google.picker.ViewId.DOCS);
                var picker = new google.picker.PickerBuilder();
                picker.addView(view);
                picker.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
                picker.setOAuthToken(token);
                picker.setAppId()
                picker.setCallback(onDriveFileOpen);
                picker = picker.build();
                picker.setVisible(true);
            }
        }

        function onDriveFileOpen(data) {
            console.log(data);
            if (data.action == google.picker.Action.PICKED) {
                var fileId = data.docs[0].id;
                console.log(fileId);
                alert(data.docs[0].name);
            }
        }

    </script>
    <script async defer src="https://apis.google.com/js/api.js">
    </script>
  </body>
</html>