Google-api-javascript-client: Client doesn't work if third-party session storage is disabled

Created on 23 Dec 2016  路  49Comments  路  Source: google/google-api-javascript-client

In Chrome (55.0.2883.95), for privacy reasons, I set "block third-party cookies and site data".

With that, the code below produces this error:

Uncaught DOMException: Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.
at x.C (https://ssl.gstatic.com/accounts/o/644096210-idpiframe.js:12:285)
at Object.k.vb (https://ssl.gstatic.com/accounts/o/644096210-idpiframe.js:8:121)
at S.h.start (https://ssl.gstatic.com/accounts/o/644096210-idpiframe.js:50:170)
at Object.Bb [as startIdpIFrame] (https://ssl.gstatic.com/accounts/o/644096210-idpiframe.js:84:713)
at https://accounts.google.com/o/oauth2/iframe:1:413

Code:

    gapi.load('client', function() {
      gapi.client.init({
        clientId: clientId,
        scope: scope
      }).then(function() {
        gapi.auth2.getAuthInstance().signIn().then(function() {
          var user = gapi.auth2.getAuthInstance().currentUser.get();
          processContacts(user.getAuthResponse()['access_token']);
        });
      });

This can be fixed by adding accounts.google.com to the exceptions list, or by allowing third-party storage wholesale, but really, this shouldn't be necessary; the client should degrade gracefully in the case where the browser doesn't allow third-party access to local storage.

auth

Most helpful comment

This thread is a bit old so wanted to comment with what appears to be the current solution.

Google's documents this as a Known Issue.

Asking users to change their browser security settings is out of the question, so the only real solution is to abandon this JS library and implement server-side Auth 2.0 flow.

Suggestion: As more browsers move toward disabling 3rd party cookies by default, this client side auth library is going to work for fewer and fewer users. This known issue about 3rd party cookies should be added as a big red warning at the beginning of the installation instructions.

All 49 comments

Thanks for reporting this issue. The issue is already known to us, but I'm not sure when or whether a fix will be available.

In the meantime, you may be able to work around this using the steps described in http://stackoverflow.com/questions/33200681/google-signin-not-working-in-safari-private-mode or something similar, although I have not tried that myself.

@bsittler, I'm pretty sure that the workaround that you linked to does not help with the current issue.

(The workaround suggests replacing the #setItem() method of Storage.prototype when the browser is in private mode, because the native method throws an error, at least in Safari. The exception that @asadovsky reported above occurs when trying to access sessionStorage, i.e. before sessionStorage.setItem() could be called.)

A different workaround to avoid the "Access is denied" exception was proposed on the Chromium issue tracker (https://bugs.chromium.org/p/chromium/issues/detail?id=357625).

That workaround replaces localStorage and sessionStorage with proxy objects, using Object.defineProperty(window, 'localStorage', {value: /*your polyfill here*/ });.

This second workaround did not work for me -- the exception is still thrown. Could this be because the function that causes the error is running in an iframe, which has a different window object from the main page?

I'd appreciate any further suggestions :)

Correct, this error occurs in an IFRAME on a separate origin, so a polyfill won't work. I was intending to suggest that you could test for working storage and avoid loading the library if the storage does not work.

Ah, gotcha, thanks for the quick response.

@mbloch @asadovsky we released a change that allows you to gracefully degrade under such environment, please have a look at the documentation: https://developers.google.com/identity/sign-in/web/reference#googleauththenoninit-onerror.

@TMSCH do you mean gracefully degrade as in show an error message telling them they need to enable 3rd party cookies?

@mjgallag yes, as you are now able to detect the issue, you can tell them that their browser is unsupported (and list potential reasons). It's very difficult to accurately detect that 3rd party data is blocked from our library, which is why we haven't yet been able to release a proper fix.

@TMSCH got it, thanks for the quick reply!

@mjgallag you're welcome!

We are experiencing this issue as well. Using the idpiframe_initialization_failed error is really helpful for us to be able to message to our users what is happening. But even if we handle the error (gapi.auth2.getAuthInstance().then(success, err => {/*handle error*/})), the gapi library still throws a javascript error, which is problematic for the following reasons:

1) What is thrown is an object, not an Error., which means there is no stacktrace associated with it. This is especially problematic for us since we log javascript errors to the server, but we don't have access to the stacktrace.
2) The object is thrown inside of a setTimeout, which means that a window.onerror handler doesn't even have access the file, line, or column of the code that threw this error. So we can't filter out all errors originating from google.com domains, because that is lost with the setTimeout.

What do you guys think of updating the library code so that it doesn't actually throw the error, but only provide the error through the getAuthInstance().then(...) error handling? Or if not that, then perhaps converting it to be a real Error object instead of a plain object?

Hi @joeldenning!
Thanks for the feedback. We actually continued to throw the error to be consistent with previous behavior, which is probably not useful anymore.
However I'd like to make sure I observe the same error when third-party data is blocked. When I reproduce it, an error is thrown from within the IFrame, not from the client window, and so I also cannot catch it (although it is an instance of Error).
Is it your case? Or is the error thrown from the main window? Also, what is the error log?
Thanks!

Would like to report we get same behaviour as @TMSCH. We cannot catch the error. Can't implement client-side OAuth2 when user doesn't allow 3rd party cookies/storage.

Hi @philovivero ! We recently pushed a changed to not throw the error from within the IFrame. Could you test again and let me know if that works? You should not observe any JS error, but be able to catch it in the promise. Thanks!
However, chrome with 3rd party cookies/storage disabled is still unsupported.

I'm not sure I will be able to repro. I ripped out all that code. For now we've decided to just let customers who uncheck that get failures, and when we look into it again, will likely implement OAuth2 server-side, since we assume at least some percentage of customers will be privacy-minded.

@TMSCH I just checked today and the error still is getting thrown when the "Block third party cookies and site data" checkbox is checked in Chrome. Here are some screenshots:

The code inside of gapi:
screen shot 2017-03-21 at 10 41 49 am

The error:
screen shot 2017-03-21 at 10 42 09 am

One thing to note is that I am using ravenjs (the js client for Sentry), which patches setTimeout with its own implementation (in order to handle errors inside of setTimeouts better). That's why you see the raven.js code in the stacktrace. For a while, I thought that maybe because I'm patching setTimeout that that may be causing the error to surface when it otherwise would not, but after some investigation I don't think that's the case. In the first screenshot, gapi is still calling setTimeout(() => {throw a}) which is something that causes an error regardless of if setTimeout is patched or not. So I don't think it's related to that, but rather just that gapi is still throwing the error.

Was the change you pushed out recently something that would prevent this from happening? It looks like the iframe is communicating the error up to the main document through a port1 onmessage handler, and then the main document is still throwing the error.

@joeldenning are you also catching the error in the initialization promise? If you don't, it will get thrown again (that's the standard behavior of a promise). I tried not catching it and observed the same behavior than you.

gapi.auth2.init({...}).catch(function(error) {
  // That's where the IFrame initialization error should be handled.
  // If you don't catch the rejection of the promise here, the error will be thrown.
});

@TMSCH Thanks, I got it work! Since gapi promises aren't real promises and don't follow the spec, the code sample you provided didn't work (It seems like you can't call .catch() on gapi promises). But if I catch it by providing two arguments to the .then(), it works.

gapi.auth2.init({...}).then(
  function success() {
  },
  function error(err) {
    // That's where the IFrame initialization error should be handled.
    // If you don't catch the rejection of the promise here, the error will be thrown.
  }
);

Thanks for your help with this and for pushing out the fix.

Oh yes, @joeldenning, you're right... Sorry about that. The return value is just a Thenable, there's no catch method. Glad you got it to work!

It would be helpful if this was covered in the docs as a known issue and potential caveat.

We recently ran into this on a project during final testing, then had to scramble a server-side flow instead. (This seems like the only workaround, other than prompting users to change security settings in their browser?)

Would've been useful to know this upfront - perhaps the could be some kind of introduction that explains pros and cons of different flows?

@bkoski we will add this to the documentation, you're right, it should be mentioned. Server-side flow is indeed the most widely supported mechanism for now, as it doesn't rely on any storage or other characteristics of the browser. Keep in mind that such option in Chrome, block third party data, is not the default and a pretty advanced feature that most users won't have enabled.

@TMSCH Privacy-conscious users enable it, and privacy-conscious tech geeks who set up systems for their less-technically-abled family and friends may also enable it. I never specifically enabled it on my system. I suspect one of the various privacy plugins I installed toggled it for me. I suspect there are many ways it becomes enabled where a non-advanced user will be the one suffering the consequence.

@bkoski I also do not know any other work-around other than implementing it server-side. I actually think it should be strongly recommended to never do client-side auth until the situation with Chrome (and other browsers?) changes.

@philovivero that's true. However, in recent browsers, the "Incognito", "InPrivate" or "Private" modes tend to have supplanted such option. It used to be very prominent in the browser's settings (I remember specifically IE), when now, it is relegated to a subcategory of advanced settings (eg Chrome), if not completely removed (I can't find the option in my version of Firefox).To be noted: in these modes and across browsers, gapi.auth2 works.

A lot of our customers are on company issued devices and this option is preset and grayed out for them.

Interestingly, this old js works fine when 3rd party cookies are disabled:

<script type="text/javascript" src="https://apis.google.com/js/client.js"></script>
<script type="text/javascript" src="https://apis.google.com/js/platform.js"></script>
...
gapi.client.setApiKey(<apiKey>);
...
// On sign in button click:
gapi.auth.authorize({
  client_id: <clientId>,
  scope: <scopes>,
  immediate: false,
  authuser: "",
}, function (authResult) {
  if (authResult.error) {
    console.log('error', authResult);
  }
  else {
    gapi.client.load('oauth2', 'v2').then(
      function (response) {
        if (response && response.hasOwnProperty('error')) {
          console.log('error', response);
          return;
        }
        else {
          var request = gapi.client.oauth2.userinfo.get();
          request.execute(function (obj) {
            var user = {
              email: obj.email,
              firstName: obj.given_name,
              lastName: obj.family_name,
            };
          });
        }
      }
    );
  }
});

@admehta01 we are implementing a solution for such environment. I'll keep this thread updated.

@TMSCH this is great news!

I think it's important to remember that some users are opted into blocking third-party cookies without their direct action, based on corporate settings pushed to them or defaults set on corporate machines. I wish we could just ignore this issue as well ("if you want it to work, just change your settings") but once it has been set as a corporate default it is difficult to make the case that it is a user issue.

@bkoski that's a good point :)

I am facing the same issue. :(

Yes, using a library that requires a client to disable privacy settings is a non-starter for us. I suspect that's why sites like StackOverflow only use Google auth with a Redirect URL and not within a pop-up. We will do the same.

This information, in my opinion, needs to be far more obvious in the documentation for web sign-in. I wasted a few hours yesterday because it seemed impossible this library wouldn't work if users are following a recommended privacy practice by disabling third-party cookies.

Hi everyone,

Chrome with third-party data blocked disables cookies in IFrames. Cookies is a required feature to be able to securely authenticate the user. Without them, it is not possible to have a pure client side library that remains secure. This is why we won't support it. Single Sign-On JS libraries generally cannot be supported in such environment (Facebook login doesn't work neither).

If many of your users have this privacy feature enabled, you can implement a server side code flow.

We added documentation regarding this: https://developers.google.com/identity/sign-in/web/troubleshooting.

We apologize for the absence of support, but there is no work around without cookies in IFrame.

@TMSCH Why do you believe this is a technical limitation? Disqus, for example, works with pop-up authentication and third party cookies blocked. Facebook works as well, contrary to your post. (E.g. go to Disqus.com and sign in with Facebook.)

I always have third party cookies disabled, and using this Google library is the first time SSO via pop-up has failed. I subsequently visited feverup.com, and they must unfortunately be using this library, because SSO silently failed there.

This does not in any way appear to be a technical limitation, unless you believe Facebook and Disqus's implementations are not secure, in which case I'd like very much to hear your reasoning!

@TMSCH Another example that works is here at Meteor's contribution agreement. "Sign in with GitHub" performs pop-up validation, even with third party cookies disabled.

It also fails at https://www.freemap.sk/?show=login :-(

The Auth V1 API (gapi.auth.authorize) used to work with third-party cookies disabled until recently (as also mentioned in https://github.com/google/google-api-javascript-client/issues/260#issuecomment-338063157).
But on March 3rd I noticed that it is no longer working. That seems like a regression.

For us this is a non-starter. We're not using the library and won't if it doesn't work with a common, positive privacy practice like disabling third party cookies.

I understand Google has a vested interest in discouraging or killing this practice since Google relies on such cookies for ad revenue. However, I'd certainly like to believe the best, that the devs here will do the right thing to support users and other developers.

@lukewlms @vladimir-i we reopened the bug as we're working on a potential (partial) solution.

Chrome with third-party data blocked disables cookies in IFrames. Cookies is a required feature to be able to securely authenticate the user. Without them, it is not possible to have a pure client side library that remains secure. This is why we won't support it. Single Sign-On JS libraries generally cannot be supported in such environment (Facebook login doesn't work neither).

So, for what it's worth, here is a quick implementation of a pure-client-side, pure-OAuth2 flow (that works with 3rd-party cookies disabled). If it serves to inspire this library's developers, wonderful; otherwise I can try to rigorize this and provide third-party support.

index.html:

<!DOCTYPE html>
<script src="https://apis.google.com/js/api.js"></script>
<script src="auth.js"></script>
<script>
const CLIENT_ID = '...';
const API_KEY = '...';
const SCOPES = ['https://www.googleapis.com/auth/calendar'];

googleAuthInit(CLIENT_ID, SCOPES).then(token => gapi.load('client:auth', () => gapiLoaded(token)));

function gapiLoaded(access_token) {
  gapi.auth.setToken({access_token});
  gapi.client.init({
      apiKey: API_KEY,
      discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'],
  }).then(() => {
    gapi.client.calendar.events.list({
        'calendarId': 'primary',
        'timeMin': '2018-04-24T00:00:00Z',
        'timeMax': '2018-04-26T00:00:00Z'}).then(response => {for (let event of response.result.items) console.log(event)});
  });
}
</script>

auth.js:

function googleAuthInit(client_id, scopes) {
  if (document.location.hash) {
    let params = {};
    for (let piece of document.location.hash.substr(1).split('&')) {
      let [k, v] = piece.split('=', 2);
      params[decodeURIComponent(k)] = v ? decodeURIComponent(v.replace(/[+]/g, ' ')) : '';
    }
    if (params.access_token && params.expires_in) {
      localStorage.setItem('access_token_expires', Date.now() + Number(params.expires_in) * 1000);
      localStorage.setItem('access_token', params.access_token);
      let base = window.location.href;
      let i = base.indexOf('#');
      if (i > -1)
        base = base.substring(0, i);
      window.history.replaceState(null, document.title, base + (params.state ? atob(params.state) : ''));
    }
  }

  function getAuthUri() {
    let base = window.location.href, state = '';
    let i = base.indexOf('#');
    if (i > -1) {
      state = base.substring(i);
      base = base.substring(0, i);
    }
    return 'https://accounts.google.com/o/oauth2/v2/auth' +
        '?client_id=' + encodeURIComponent(client_id) +
        '&redirect_uri=' + encodeURIComponent(base) +
        '&state=' + encodeURIComponent(btoa(state)) +
        '&response_type=' + encodeURIComponent('token') +
        '&scope=' + encodeURIComponent(scopes.join(' ')) +
        '&include_granted_scopes=' + encodeURIComponent('true');
  }

  let access_token_expires_in = Number(localStorage.getItem('access_token_expires')) - Date.now() - 1000;
  if (access_token_expires_in < 0) {
    window.location.replace(getAuthUri());
    return Promise.reject();
  }

  window.setTimeout(() => window.location.replace(getAuthUri()), access_token_expires_in);
  return Promise.resolve(localStorage.getItem('access_token'));
}

Here's a drop-in version of auth.js that should work without changes to the [current] examples. It first tries upstream gapi.auth2.init, but catches idpiframe_initialization_failed and tries conventional OAuth2, then shoves the access token back in via gapi.auth.setToken and forces gapi.auth2.getAuthInstance().isSignedIn.get() to return true. I'd love for upstream gapi.auth2.init to do something similar (is the uncompiled code for at least the auth2 component available?), but if that's not possible I can keep working on this, just let me know!

(function() {

var patchedScriptOnload = false;
var patchedGapiLoad = false;
var patchedGapiAuth2Init = false;

tryPatchGapi();

function tryPatchGapi() {
  if (!window.gapi) {
    if (!patchedScriptOnload) {
      for (let script of document.getElementsByTagName('script')) {
        if (script.src.match(/^https:[/][/]apis[.]google[.]com[/]js[/]/)) {
          patchedScriptOnload = true;
          if (script.onload) {
            console.log('gapi-issue260: Installing onload patcher to', script);
            let scriptOnload = script.onload;
            script.onload = function(e) {
              tryPatchGapi();
              scriptOnload(e);
            };
          } else {
            console.log('gapi-issue260: Installing "load" event patcher to', script);
            script.addEventListener('load', tryPatchGapi);
          }
        }
      }
    }
    return;
  }

  if (!gapi.auth2) {
    if (!patchedGapiLoad) {
      patchedGapiLoad = true;
      console.log('gapi-issue260: Patching gapi.load.');
      let gapiLoad = gapi.load;
      gapi.load = function(targets, callback) {
        console.log('gapi-issue260: Watching gapi.load(', targets, ', ', callback, ').');
        if (callback instanceof Function) {
          gapiLoad(targets, () => {
            tryPatchGapi();
            callback();
          });
        } else {
          var newCallback = {
            callback: () => {
              tryPatchGapi();
              callback.callback();
            },
            onerror: callback.onerror,
          };
          gapiLoad(targets, newCallback);
        }
      };
    }
    return;
  }

  if (patchedGapiAuth2Init)
    return;
  patchedGapiAuth2Init = true;
  console.log('gapi-issue260: Patching gapi.auth2.init.');
  let gapiAuth2Init = gapi.auth2.init;
  gapi.auth2.init = function(params) {
    console.log('gapi-issue260: Watching gapi.auth2.init(', params, ').');
    return new Promise((resolve, reject) => {
      gapiAuth2Init(params).then(
          resolve,
          e => {
            if (e.error == 'idpiframe_initialization_failed') {
              console.log('gapi-issue260: Caught', e, '-- trying workaround.');
              workaround(params).then(access_token => {
                console.log('gapi-issue260: Success! Switching to gapi.auth and continuing.');
                gapi.auth.setToken({access_token});
                gapi.auth2.getAuthInstance().isSignedIn.get = () => true;
                resolve();
              });
            } else {
              reject(e);
            }
          });
    });
  };
}

function workaround(params) {
  if (document.location.hash) {
    let params = {};
    for (let piece of document.location.hash.substr(1).split('&')) {
      let [k, v] = piece.split('=', 2);
      params[decodeURIComponent(k)] = v ? decodeURIComponent(v.replace(/[+]/g, ' ')) : '';
    }
    if (params.access_token && params.expires_in) {
      localStorage.setItem('access_token_expires', Date.now() + Number(params.expires_in) * 1000);
      localStorage.setItem('access_token', params.access_token);
      let base = window.location.href;
      let i = base.indexOf('#');
      if (i > -1)
        base = base.substring(0, i);
      window.history.replaceState(null, document.title, base + (params.state ? atob(params.state) : ''));
    }
  }

  function getAuthUri() {
    let base = window.location.href;
    let state = '';
    let i = base.indexOf('#');
    if (i > -1) {
      state = base.substring(i);
      base = base.substring(0, i);
    }
    return 'https://accounts.google.com/o/oauth2/v2/auth' +
        '?client_id=' + encodeURIComponent(params.client_id) +
        '&redirect_uri=' + encodeURIComponent(base) +
        '&state=' + encodeURIComponent(btoa(state)) +
        '&response_type=token' +
        '&scope=' + encodeURIComponent(params.scope) +
        '&include_granted_scopes=true';
  }

  let access_token_expires_in = Number(localStorage.getItem('access_token_expires')) - Date.now() - 1000;
  if (access_token_expires_in < 0) {
    window.location.replace(getAuthUri());
    return Promise.reject();
  }

  window.setTimeout(() => window.location.replace(getAuthUri()), access_token_expires_in);
  return Promise.resolve(localStorage.getItem('access_token'));
}

})();

Can Anyone help me with this Issue?

I have to find out the issue that why website does not load the page properly after sign in with google api but when i run the project it works fine on my localhost

@UserDac Hi, can you be more specific regarding your issue? Can you describe what happens?

We have 587 users that experienced this problem, it would be nice to fix this in the library 馃洜.

@TMSCH Any news on progress made in the last year?

Not a fix, but at least it won't fail silently.

I have found that a message is posted to the parent window that indicates that cookies are not enabled when the page is first loaded. Only tested in Chrome with Third Party cookies turned off.

method: "fireIdpEvent"
    params: 
        error: "Cookies are not enabled in current environment."

This is basic code and assumes if here is an error, that the error is a cookie error which I'm sure is not always the case, and I'm no JS expert... So modify as needed.

window.addEventListener('message', function(event) {
    if(typeof(event.data) != "undefined") {
        var msg_json = JSON.parse(event.data);
        if(typeof(msg_json.params) != "undefined") {
            if(typeof(msg_json.params.error) != "undefined"){
                // there is an error warn the user
                $('#cookie_warn').html('You must allow third-party cookies in your browser before you can log in');
            }
        }
    }
});

@TMSCH @grant Any progress on this? Would be great to see a fix for this as part of the upcoming security audit.

Any news or progress related to this issue? Is there at least a confirmed workaround until the issue is not completely fixed? Thanks.

I pasted a workaround with some old js code earlier (not sure if it still works):
https://github.com/google/google-api-javascript-client/issues/260#issuecomment-338063157

My solution was to move everything to the backend:
https://developers.google.com/identity/sign-in/web/backend-auth

No issues with it so far.

See my comment here. May help in your case

This thread is a bit old so wanted to comment with what appears to be the current solution.

Google's documents this as a Known Issue.

Asking users to change their browser security settings is out of the question, so the only real solution is to abandon this JS library and implement server-side Auth 2.0 flow.

Suggestion: As more browsers move toward disabling 3rd party cookies by default, this client side auth library is going to work for fewer and fewer users. This known issue about 3rd party cookies should be added as a big red warning at the beginning of the installation instructions.

Google Chrome incognito blocks 3rd party cookies by default now

Was this page helpful?
0 / 5 - 0 ratings