6

Firstly, I have reviewed multiple SO questions relating to similar issues and the Google Picker API docs but cannot find a solution. Links at the bottom of this post.

Goal

After using the Google auth2 JavaScript library to complete a ux_mode='popup' Sign In process for a user to obtain an access_code, I wish to open a Google Picker window using the picker JavaScript library and the obtained access_code.

Expected

After checking the access_code is still valid, when the google.picker.PickerBuilder() object is set to visible via picker.setVisible(true) the user is ALWAYS able to select files as per the configuration set for the picker object.

Actual

On a fresh browser, encountering this flow results in the Google Picker window asking the user to sign in again. If the user chooses to do so an additional popup will be triggered that automatically executes a login flow again and the user is then informed that "The API developer key is invalid."

What is truly unexpected about this is that if the user refreshes the page and repeats the exact same actions the Google Picker works exactly as expected. The only way to replicate the anomalous behaviour is to clear all the browser cache, cookies and any other site related settings. Picker asking user to sign in again to their Google account

On the JavaScript console there are the common errors of:

Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘https://docs.google.com’) does not match the recipient window’s origin (‘http://localhost’).

Invalid X-Frame-Options header was found when loading “https://docs.google.com/picker?protocol=gadgets&origin=http%3A%2F%2Flocalhost&navHidden=true&multiselectEnabled=true&oauth_token=.............: “ALLOW-FROM http://localhost” is not a valid directive.

But otherwise, no other indication of error in the console, and the exact same errors are reported when the Picker works exactly as expected.

List of things I have confirmed

  • I have added the Google Picker API to the associated project in the Google developer console
  • I am working with a validated OAuth application configured in the OAuth Consent Screen
  • I have tried working with both localhost and an https:// top level domain with valid cert and registered with the Google console as verified
  • I have generated an API Key that is explicitly associated with the Picker API and the relevant URLs
  • The API key is set as the .setDeveloperKey(APIKey)
  • The API key does show usage in the GCP console
  • The clientId is correct and the appId is correct for the GCP project
  • I have tried with scopes of ['https://www.googleapis.com/auth/drive.file'] and with scopes of ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/documents']

Attempts to Resolve

I can replicate this behaviour with the bare minimum example provided in the docs Google Picker Example used as a direct cut and paste, only replacing the required credential strings.

Right before invoking the picker = new google.picker.PickerBuilder() I have validated the access_token by executing a GET fetch to https://www.googleapis.com/oauth2/v1/tokeninfo and the signin behavior still results when this returns a successful validation of the token.

I check the token using this simple function:

function checkToken(access_token) {
    fetch("https://www.googleapis.com/oauth2/v1/tokeninfo", {
        method: "GET",
        headers: {
            'Authorization': 'Bearer ' + access_token
        }
    }).then(response => {
        if (response.status === 200) {
            return response.json();
        } else {
            console.log('User Token Expired');
            resetLoginCache();
        }
    }).then(data => {
        console.log('User Token Still Valid');
    }).catch((error) => {
        console.error('User Token Check Error:', error);
    })
}

The JavaScript API's are initialized with:

<meta name="google-signin-client_id" content="<clientId>">
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
function initApi() {
    gapi.load('signin2:auth2:picker', () => {
        window.gapi.auth2.init(
            {
                'client_id': clientId,
                'scope': scope.join(" ")
            });
    });
};

In my application code I've (poorly) implemented a simple generalization of a picker builder:

// Use the Google API Loader script to load the google.Picker script.
function loadPicker( mimeTypes = null, callback = pickerCallback ) {
    gapi.load('picker', {
        'callback': () => {
            console.log("Gonna build you a new picker...")
            createPicker( mimeTypes, callback );
        }
    });
}

// Create and render a Picker object for searching images.
function createPicker( mimeTypes, callback ) {
    let gAuthTop = gapi.auth2.getAuthInstance();
    let gUser = gAuthTop.currentUser.get();
    let gAuthResp = gUser.getAuthResponse(true);
    console.log(gAuthResp.access_token);
    checkToken(gAuthResp.access_token)
    if (pickerApiLoaded && oauthToken) {
        // based on MIME type:
        // FOLDER => google.picker.DocsView(google.picker.ViewId.FOLDERS)
        //      Cannot set mimeTypes to filter view
        // DOCS => google.picker.View(google.picker.ViewId.DOCS)
        //      Can set MIME types to match
        let selectView = new google.picker.View(google.picker.ViewId.DOCS);
        if (mimeTypes) {
            if (mimeTypes.includes(gdriveFolderMIME)) {
                selectView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
                selectView.setIncludeFolders(true);
                selectView.setSelectFolderEnabled(true);
                selectView.setParent('root');
            } else {
                selectView.setMimeTypes(mimeTypes);
            }
        }
        let picker = new google.picker.PickerBuilder()
            .enableFeature(google.picker.Feature.NAV_HIDDEN)
            .enableFeature(google.picker.Feature.MINE_ONLY)
            .setAppId(appId)
            .setMaxItems(1)
            .setOAuthToken(gAuthResp.access_token)
            .addView(selectView)
            .setSelectableMimeTypes(mimeTypes)
            .setDeveloperKey(developerKey)
            .setOrigin(window.location.protocol + '//' + window.location.host)
            .setCallback(callback)
            .setTitle('Application Title')
            .build();
        console.log('Origin was set to: ', window.location.protocol + '//' + window.location.host)
        picker.setVisible(true);
    }
}

I've even tried to dig into the minified code loaded by the Picker but I'm not that good at JavaScript and the Firefox debugger wasn't able to help me understand what might be triggering this. However, once the "error" has been passed once, it will not appear on the same browser again for any user account and within Firefox using Private Mode will also no longer show the sign in error until all history, cookies and cache are cleared.

As proof I have done some reasonable research, similar SO questions that I have reviewed and tried working with are:

As well as the following documentation:

arkore
  • 105
  • 12
  • Hello there @arkore, what type of credentials did you choose for the Picker? Moreover, I found this issue [here](https://issuetracker.google.com/issues/120462210) that seems similar to yours. Can you confirm this? – ale13 Sep 24 '20 at 08:08
  • Hi @ale13, that issue does actually look somewhat similar to mine, but no resolution is provided after 2 years. The credentials I am using are those generated by going through a standard JavaScript client sign in flow, triggered with the `grantOfflineAccess()` method but the error can be replicated using the example in the Picker documentation guide. I am using an API key generated for the same GCP project that the Auth flow credentials are associated with. i.e. same client_id, same project_id – arkore Sep 25 '20 at 01:52
  • Hey @ale13, I have further reviewed in detail the discussion that was in the linked unresolved issue. Assuming that maybe the Picker library has some dependency on older JS libraries or the Drive API I have tried adding these to the [minimal example](https://developers.google.com/picker/docs#example) at the bottom of the Picker Docs (which also doesn't use `grantOfflineAccess()`) and still the bug persists. – arkore Sep 25 '20 at 02:52
  • 1
    What type `access_type` does the token you are using have? Is it `access_type=offline` or `access_type=online`? @arkore – ale13 Sep 30 '20 at 15:05
  • 1
    @ale13 for my application I have been using `access_type=offline` by utilizing the `grantOfflineAccess()` method of the `auth2` library. However, for the minimal example from Google's official docs, this uses `access_type=online` by utilizing `authorize()` method of the `auth` library. I can reproduce the issue in both cases. – arkore Oct 01 '20 at 07:37
  • You might want to check this answer [here](http://issuetracker.google.com/issues/166478547#comment6) @arkore – ale13 Oct 02 '20 at 12:30
  • 1
    Thanks for your continued interest @ale13, the [minimal example](https://developers.google.com/picker/docs#example) from the Google Picker docs uses `if (pickerApiLoaded && oauthToken) {` within the `function createPicker() {` function which I can use to recreate the error. I also explicitly check the `access_token` validity before building the Picker in my own version of the code. I am relatively confident the cause isn't the token being invalid, but could be something unknown to me about the nature of the token? However, the docs must be wrong as the error persists with their example. – arkore Oct 03 '20 at 13:46
  • 1
    @arkore - the fact that the error reemerged after clearing browser data indicates that the issue is likely to be an authentication cookie problem. After a refresh on a clean browser they are set, and upon deletion of all data they are not. Your setup looks ok to me, just one question, though - do you actually use the `oauthToken` variable? Where is it defined? – Oleg Valter is with Ukraine Oct 03 '20 at 15:16
  • 1
    @OlegValter in my own code I am actually using the Svelte framework and `oauthToken` is actually a local listener to a global store variable. It is set and updated with `gAuthResp.access_token` anytime the user signs in or out. I will note that the same behaviour occurs in the [minimal docs example](https://developers.google.com/picker/docs#example) which doesn't include this additional complexity. Also note that in my code, `checkToken()` will clear `oauthToken` if it is found to be expired by directly checking Googles OAuth servers. – arkore Oct 04 '20 at 23:55
  • 1
    @arkore - just checked, never hurts to ask. Your question is extremely well-researched, which is a rarity in itself. I wonder, can you reproduce the error when using Chrome? As it looks suspiciously like a cookies issue and that it may be vendor-specific. – Oleg Valter is with Ukraine Oct 06 '20 at 02:41
  • 2
    @OlegValter, thanks for your prompting. Wanting to nail down this bug, I spun up an EC2 instance with Windows Server 2019 and freshly installed Chrome, FireFox and Edge. I was struggling to recreate the issue on Chrome unless I used Private Mode, could not recreate it on FireFox but it occured in all cases on Edge. The bug was originally reported to me by a user with Chrome on Mac and on my own system I can still recreate it on FireFox but in Private Mode only. It does seem to be browser specific to some degree but I can't see the pattern. – arkore Oct 13 '20 at 03:05

0 Answers0