google / google-api-javascript-client

Google APIs Client Library for browser JavaScript, aka gapi.
Apache License 2.0
3.22k stars 1.06k forks source link

gapi.auth2.authorize calls callback immediately, with error="unknown_error" #367

Open SinusPi opened 6 years ago

SinusPi commented 6 years ago

As soon as I call gapi.auth2.authorize, it opens a pop-up AND calls the callback with an unknown_error, instead of waiting for the result of the pop-up. Also, there doesn't seem to be a way to use ux_mode:"redirect" with .authorize(), is there?

The (simplified) code I'm using is:

<script src="https://apis.google.com/js/platform.js?onload=onLoad" async defer></script>
<a href="#" onclick="gpLogIn()">Login with Google</a>
<script>
  function onLoad() {
    gapi.load('auth2');
  }
  function gpLogIn() {
    gapi.auth2.authorize({
      client_id: '0000000000000000000000.apps.googleusercontent.com',
      scope: 'email profile openid',
      response_type: 'id_token permission',
      //ux_mode: 'redirect',  // sadly, no go, but that's another story
    }, function(response) {
      console.log(response);
    });
  }
</script>

This fails under Chrome 62 under Windows 10. Works correctly under Firefox Quantum 57, though.

TMSCH commented 6 years ago

@SinusPi what browser/version/platform are you encountering this issue under?

SinusPi commented 6 years ago

Chrome 62, Windows 10. I've just tested it to work correctly under Firefox Quantum 57; updating issue description.

SinusPi commented 6 years ago

... And right after having tested it on Firefox, it started working under Chrome as well. Sorcery, I tell you. I guess I'll come back when it breaks again...

TMSCH commented 6 years ago

Next time it happens, could you check the Record logs and the full console logs displayed?

SinusPi commented 6 years ago

Console logs only consisted of an Object with .error="unknown_error" that my console.log call output, there was nothing else printed there at the moment of the pop-up call. I'll try the Record next time.

euri10 commented 6 years ago

I have the very same error on chromium version 62.0.3202 , 63.0.3239 and 64.0.3282.119 when I copy past the hello world drive picker app from here https://developers.google.com/picker/docs/ : It works fine on Firefox and chrome 64.0.3282.119

{error: "unknown_error", status: {…}, g-oauth-window: Window, client_id: "id", cookie_policy: "single_host_origin", …}
client_id
:
"id"
cookie_policy
:
"single_host_origin"
error
:
"unknown_error"
g-oauth-window
:
Window
blur
:
ƒ ()
close
:
ƒ ()
closed
:
true
focus
:
ƒ ()
frames
:
null
length
:
0
location
:
Location {href: undefined, assign: ƒ, replace: ƒ}
opener
:
null
parent
:
null
postMessage
:
ƒ ()
self
:
null
top
:
null
window
:
null
response_type
:
undefined
status
:
{signed_in: false, method: null, google_logged_in: false}
__proto__
:
Object
gregsabo commented 6 years ago

Our existing integration has started firing this unknown_error sporadically. It returns this error about 1/5 of the time.

TMSCH commented 6 years ago

@gregsabo could you provide a snippet of code that reproduces the issue? Would you have some Network logs to provide (esp. the ones to https://accounts.google.com/o/oauth2/...)

@euri10 the snippet of code in this documentation is unfortunately both wrong and outdated... Apology for that. gapi.auth.authorize is deprecated, gapi.auth2.authorize should be used.

The call to this method, in the given snippet, would be:

gapi.auth2.authorize({
  'client_id': clientId,
  'scope': scope
}, handleAuthResult);

This method will open a popup: this has to be done synchronously after a user interaction, otherwise the browser can block the popup. Most commonly, this call has to be triggered after the user clicks a button.

euri10 commented 6 years ago

localhost.txt Thanks @TMSCH I changed the call to api2 but I get the same unknown_error on chromium 64.0.3282.119 while on chrome same version it works fine. I go past the password and in the handleAuthResult function it now return this object (left side is chrome working fine, right is chromium), see the whole code below, maybe it's a misuse but it's still weird it works on kind of all borwser I tested but chromium.

2018-01-31-091716_1912x1072_scrot

<html>
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script type="text/javascript">
        // The Browser API key obtained from the Google API Console.
        var developerKey = 'EDITED';
        // The Client ID obtained from the Google API Console. Replace with your own Client ID.
        var clientId = "EDITED";
        // Scope to use to access user's photos.
        var scope = 'https://www.googleapis.com/auth/drive.readonly.metadata';
        var pickerApiLoaded = false;
        var oauthToken;

        // Use the API Loader script to load google.picker and gapi.auth.
        function onApiLoad() {
            console.log('before gap load');
            gapi.load('auth', {'callback': onAuthApiLoad});
            gapi.load('picker', {'callback': onPickerApiLoad});
        }

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

        function onPickerApiLoad() {
            console.log('onPickerApiLoad');
            pickerApiLoaded = true;
            createPicker();
        }

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

        function createPicker() {
            console.log('createPicker');
            if (pickerApiLoaded && oauthToken) {
                var docsView = new google.picker.DocsView()
                    .setIncludeFolders(true)
                    .setMimeTypes('application/vnd.google-apps.folder')
                    .setSelectFolderEnabled(true);
                var picker = new google.picker.PickerBuilder().addView(docsView).setOAuthToken(oauthToken).setDeveloperKey(developerKey).setCallback(pickerCallback).build();
                picker.setVisible(true);
            }
        }

        // A simple callback implementation.
        function pickerCallback(data) {
            console.log('pickerCallback');
            if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
                var doc = data[google.picker.Response.DOCUMENTS][0];
                folderid = doc[google.picker.Document.ID];
                foldername = doc[google.picker.Document.NAME];
                console.log(folderid);
                $.ajax({
                    url: '{{ url_for('settings') }}',
                    data: JSON.stringify({
                        'folderid': folderid,
                        'foldername': foldername
                    }),
                    type: 'POST',
                    contentType: 'application/json',
                    success: function (response) {
                        console.log(response);
                        console.log('success');
                        $('#update-div').html('Folder set to ' + foldername + ' with id ' + folderid);
                    },
                    error: function (error) {
                        console.log(error);
                    }
                });
            }
        }
    </script>
</head>
<body>

<div id="update-div"></div>

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

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

edit: added localhost.har renamed as txt file localhost.txt file for the failing chromium, if that helps

TMSCH commented 6 years ago

@euri10 thanks for the snippet of code and the HAR, it's very helpful. It seems that the library doesn't properly initialize, as I can't see some specific requests in the HAR file.

I tried your code with Chromium on Debian. The popup was blocked upon opening the app (which is expected). When manually triggering onAuthApiLoad from the console, I can successfully authorize the user. I'm using Chromium Version 66.0.3336.0 (Developer Build) (64-bit).

However I can reproduce your error after enabling "Block third-party cookies" in Settings. gapi.auth2 does not currently work when this option is enabled. Is it the case for you?

TMSCH commented 6 years ago

@gregsabo it might be due to third-party data blocked. Do you know if your user base could sometimes have this setting enabled? Enterprise users maybe?

euri10 commented 6 years ago

@TMSCH thanks, the Block third-party cookies indeed is what caused the unknown_error for me, unsetting it (it's enabled by default on chromium, no idea on other browsers) solves the issue !!

gregsabo commented 6 years ago

@TMSCH I doubt blocking third-party cookies is the culprit since users who have successfully logged in to our app with Google for months are suddenly reporting this problem. We've confirmed that users are having trouble in Chrome, Firefox, and Edge.

TMSCH commented 6 years ago

@gregsabo are you using gapi.auth.authorize library? Or gapi.auth2.authorize?

If so, please provide a snippet of code with the configuration of the call you make. Thanks!

TMSCH commented 6 years ago

Is there any chance I could try a live URL of your app? We made some changes in gapi.auth.authorize that might have created this issue so debugging myself would be very useful.

gregsabo commented 6 years ago

Here's a live URL:

When the bug happens, we display "Google Authentication Failed" in a pink box on the login screen.

We're using gapi.auth.authorize. I'll work on getting the snippet. Do you mean the client ID and such?

gregsabo commented 6 years ago

Actually, here are some easier steps to reproduce:

TMSCH commented 6 years ago

@gregsabo I get to that screen with the error message you mention after closing the popup once or twice. It seems that the login page triggers it when the call to gapi.auth.authorize fails.

A recent change we made starts exposing a popup_closed_by_user error. It was previously silently ignored. It seems to be the case here, not an unknown_error.

screen shot 2018-02-01 at 5 40 47 pm

TMSCH commented 6 years ago

@gregsabo are you still observing the unknown_error? We pushed changes that should expose more info on the error.

jadengeller-asana commented 6 years ago

@TMSCH We're noticing a new error now, so I guess that's promising!

We had a developer here run into the bug, and it reproduced consistently in his browser. The error returned by gapi.auth2.authorize (yes, we upgraded to auth2) is "user_logged_out". This is pretty surprising because the user is logged into multiple accounts. They are able to successful select an account (They are not prompted for password because they are already logged in.), but when the popup closes, the response returned in the callback indicates the user is logged out.

We played around with the prompt argument and found that, if "none" is specified, we get an "immediate_failed" error with subtype "no user_bound". The user has definitely authenticated. They're able to access Google services in their browser. They are not asked for a password when choosing an account in the Google auth popup.

This developer claims that they were able to solve the issue by clearing just their asana.com cookies. This seems really weird. The odd behavior we're seeing is entirely isolated to gapi.

If you have an idea what might be going wrong, we'd really appreciate it.

The issue doesn't reproduce consistently, but feel free to use https://app.asana.com/-/login?use_gauth2=true to try out this auth flow. (That query parameter is necessary since we've switched back to the redirect-based flow for our users as mitigation for this issue.)

TMSCH commented 6 years ago

Thanks for upgrading to auth2!

The prompt: 'none' raising immediate_failed if the popup flow didn't work makes sense. I'm very surprised by the error though after the user goes through the popup flow.

I can't reproduce it myself, but next time it happens, could you check the Console logs for requests to https://accounts.google.com/o/oauth2/iframerpc?..., especially for ?action=issueToken or ?action=listSession, and let me know what the content of the request as well as the response are?

Thanks!

jadengeller-asana commented 6 years ago

@TMSCH I appreciate the response! We were able to reproduce the issue internally again.

I set a breakpoint on window close and checked preserve logs. Only one request showed up in the network tab—a request to https://accounts.google.com/_/signin/oauth. I didn't see the things you mentioned.

BUT, the error response we received this time was different! I'm fairly sure this is the same issue, so I'm assuming something changed in the error reporting in the server-side code.

{error: "immediate_failed", detail: "Not all scopes are approved.", thrown_by: "server"}

This is a super interesting error response since the only scopes we're requesting are "email" and "profile". AND, when I repeat the call to authorize omitting the "profile" scope, it succeeds!

It's not actually clear to me what this error message indicates. I thought "profile" and "email" were available to all client ids. It also seems super duper bizarre to me that our login issue is spurious, both in that most users don't see it and in that most users that see it don't see it consistently.

If you have further insight, I'd really appreciate it. Thanks so much!

TMSCH commented 6 years ago

@jadengeller-asana hmm this error can happen for several reasons. If you sign in, revoke the scopes in myaccount.google.com, then come back to the app and ask for prompt: 'none' it could raise this error. However it wouldn't work if you ask for email right after. Could I see a snippet of code that shows the calls to gapi.auth2.authorize? And what it was, before the migration?

Are previously signed in users (without revoking scopes) able to sign in with gapi.auth2.authorize? Or are all facing the issues you're having?

jadengeller-asana commented 6 years ago

I was manually running

gapi.auth2.authorize({ client_id: "OUR_CLIENT_ID_HERE", scope: "profile email" }, function(x) { console.log(x); })

in the JS console in Chrome.

jadengeller-asana commented 6 years ago

Are previously signed in users (without revoking scopes) able to sign in with gapi.auth2.authorize?

I don't believe the users having these issues revoked any scopes. I can confirm that profile was failing while email was succeeding since I was able to meet with a user with a repro.

TMSCH commented 6 years ago

@jadengeller-asana had you clear the cache when running these tests?

Could you send me the HAR of the network requests made when reproducing the issue so I can check what could be wrong?

indrekru commented 6 years ago

Hello, to emphasize this, I seem to be encountering the same issue. It pops up the signIn modal via calling: GoogleAuth.signIn() I select one of my accounts, modal closes and I see in network calls: {"error":"USER_LOGGED_OUT","detail":"No active session found."} from: https://accounts.google.com/o/oauth2/iframerpc?action=issueToken&response_type=token... It's a 200 OK, no exceptions thrown, no promise resolved from signIn().then.

TMSCH commented 6 years ago

@indrekru do you have a snippet of code or a live app so I can reproduce the issue?

indrekru commented 6 years ago

Hey again, figured out what's going on. I have Ghostery browser extension, which seems to mess with your client library flow. Sorry for maybe causing too much alarm. BUT here's my fundamental question: How can I ensure my customers 100% working login flow, if I don't know what browser extensions might be messing with the HTTP traffic? I also tried adblock, that one works. So I have no overview whatsoever what else might be out there and what it might be doing with your client flows.

Seems the only sensible thing to do is to fallback to the redirect flow. Or am I wrong?

TMSCH commented 6 years ago

@indrekru thanks for finding the culprit! It is very difficult to ensure it works if some browsers extensions are messing with the HTTP Traffic in such a way. IMO, this is an issue with the extension, not the library. I also use adblocker and never had any problems. I'd suggest bringing the issue to the author of Ghostery.

jadengeller-asana commented 6 years ago

had you clear the cache when running these tests?

I don't think we did. Why does this matter?

TMSCH commented 6 years ago

While running manual testing on the library, especially when changing scopes and other config, it can sometimes lead to some edge cases due to caching that are basically not observable in production apps were configuration is stable.

jadengeller-asana commented 6 years ago

Okay, I'll be sure to clear the cache next time I observe the issue.

jadengeller-asana commented 6 years ago

Could you send me the HAR of the network requests made when reproducing the issue so I can check what could be wrong?

I have a textual copy of the headers from the POST request to https://accounts.google.com/_/signin/oauth, but I didn't save a HAR. If that's useful, I'll send it over (how?). If not, I'll collect a HAR when I again have access to a repro.

jadengeller-asana commented 6 years ago

I asked one of our repro users about any interesting browser extensions they have installed. They mentioned Privacy Badger. It seems very likely that could be causing issues here.

TMSCH commented 6 years ago

@jadengeller-asana did you manage to see whether Privacy Badger was the culprit of the issue?

jadengeller-asana commented 6 years ago

Not yet, but there's at least 2 distinct issues here. One repro user gets a "Not all scopes are approved." and another repro user gets a "user_logged_out" error. The Privacy Badger extension is installed for the latter user.

TMSCH commented 6 years ago

@jadengeller-asana the "Not all scopes are approved" might be due to the fact the user had already signed in with GSI v1 but it's surprising. I'd need the network requests of calls to https://accounts.google.com/o/oauth2/iframerpc to debug, would you have them?

jadengeller-asana commented 6 years ago

I only recall seeing requests to https://accounts.google.com/_/signin/oauth, even when I preserved log. Am I mistaken?

TMSCH commented 6 years ago

That's very surprising, at least one call to the URL I mentioned is required, and is done right after the IFrame is loaded on the page.

jadengeller-asana commented 6 years ago

Oh, I think I only started recording network activity after that point (since I cannot open the developer tools before the page is opened). I guess I'll have to use something other than Chrome developer tools.

TMSCH commented 6 years ago

You can open the tool and then redirect to your page. Those calls happen in the main window, not the popup.

c58 commented 6 years ago

@TMSCH I probably have the same issue: Using this snippet:

   window.gapi.auth2.authorize({
        client_id: CLIENT_ID,
        scope: 'profile email',
        response_type: 'id_token permission'
      }, (response) => {
        // Here...
        // response.error === 'user_logged_out'
      });

Also tried to use signIn(), and it returns promise which never resolved/rejected. The device is iOS/Safari. Tried in Safari/"incognito" – works. Tried in iOS/Chrome – works. And some days ago i remember i was able to login in iOS/Safari, but looks like something happened and now it is not working. Maybe it is related with multiple Google accounts that i have logged in inside iOS//Safari (one personal and one work), and the personal one was logged out (maybe expired), but internally you checked the other one which is logged in and not expired, but returned a token to the one which was logged out which is not working... Something like that, just a guess :)

Here is the output from iframe, if it can help (i replaced actual id_token and domain to a placeholder):

window.addEventListener('message', function(event) {
  console.log(event.data);
});
[Log] {"method":"fireIdpEvent","params":{"type":"idpReady"},"rpcToken":"379831079.4531398"} (vendor.js, line 39)
[Log] {"id":"568-82480.7010874615","result":true,"rpcToken":"379831079.4531398"} (vendor.js, line 39)
[Log] {"method":"fireIdpEvent","params":{"type":"authResult","clientId":"137802694133-hapokd2n5ktivbpfb7oehgre4bhd4usp.apps.googleusercontent.com","id":"auth848489","authResult":{"id_token":"<id_token_here>","login_hint":"AJDLj6K83uPn1JhzuwTegx9N1-BA8V4khmiQP0sUt6SFvvCkpLPQLrdIpZ22GRhMqaX9Q3VyyK6ZN8PwJv1i-NE0v5XwXrpDs71sWpy6sdhDomnX4Y994O0","client_id":"137802694133-hapokd2n5ktivbpfb7oehgre4bhd4usp.apps.googleusercontent.com"}}} (vendor.js, line 39)
[Log] {"id":"638-527205.5765403978","rpcToken":"379831079.4531398"} (vendor.js, line 39)
[Log] {"method":"fireIdpEvent","params":{"type":"sessionSelectorChanged","newValue":{"hint":"AJDLj6K83uPn1JhzuwTegx9N1-BA8V4khmiQP0sUt6SFvvCkpLPQLrdIpZ22GRhMqaX9Q3VyyK6ZN8PwJv1i-NE0v5XwXrpDs71sWpy6sdhDomnX4Y994O0","disabled":false},"domain":"https://mydomain.com","crossSubDomains":true},"rpcToken":"379831079.4531398"} (vendor.js, line 39)
[Log] {"id":"641-316556.7442235234","result":true,"rpcToken":"379831079.4531398"} (vendor.js, line 39)
[Log] {"id":"644-33757.89449158148","result":{"error":"user_logged_out"},"rpcToken":"379831079.4531398"} (vendor.js, line 39)
jadengeller-asana commented 6 years ago

@TMSCH I got the HAR! Is there a way I can share it with you without posting it here? I don't want to accidentally share personally identifiable information publicly.

jadengeller-asana commented 6 years ago

Really interestingly, gapi.auth2.authorize failed with the error popup_closed_by_user but gapi.auth.authorize succeeded. Maybe we shouldn't have upgraded...

SinusPi commented 6 years ago

@c58 , this is a bit beside the point, but you might want to be careful about that (response) => { } lambda function there; IE still doesn't support that.

c58 commented 6 years ago

@SinusPi it is transpilled with Babel, but thanks for the precaution anyway ;)

jadengeller-asana commented 6 years ago

@TMSCH Are you still interested in checking out the HAR? We'd really appreciate understanding what's going wrong. We've stopped using this library (and the popup-based flow) in production entirely for the time.

jadengeller-asana commented 6 years ago

We've been able to somewhat isolate what Google-set cookies that are causing the issue.

One user with the issue deleted the following cookies one at a time, and Google SSO started working after the final cookie was deleted:

Another user with the issue didn't have the _utma cookie, so I asked the follow the same exercise of deleting Google-set cookies one at a time. They deleted the following, and things started working after the final cookie:

Hopefully this information is useful in debugging the issue.

fernandohu commented 6 years ago

I have this problem too in Xiaomi Mi UI OS (Xiaomi Redmi phones). Using the Google Chrome app I am able to use the Google Picker correctly.

But if I change to the OS native browser (it is a very decent browser), Google Picker will not launch. It just keeps returning "user_logged_out". I've played around a little bit but could not find a solution.

Dropbox Integration with Chooser works very well on all platforms. I can't understad why Google Picker is so bugged.