4

I'm using Google Identity Services JavaScript API to access some of my Google data here:

https://developers.google.com/identity/oauth2/web/guides/migration-to-gis

The code I used is in this section:

Implicit flow examples > The new way > GAPI async/await.

I ran the code from a http://localhost:xxxx and I have added the domain and port to the related credentials.

Everything works fine:

  1. I can see my profile image and name after logging in.
  2. I can access the expected data after logging in.

But I have to log in again after refreshing the page.

Is there any way to keep the login status in my browser so that I don't have to click my username in that consent popup again and again repeatedly?

Code below:

<!DOCTYPE html>
<html>
<head></head>
<body>
  <h1>GAPI with GIS async/await</h1>
  <button id="showEventsBtn" onclick="showEvents();">Show Calendar</button><br><br>
  <button id="revokeBtn" onclick="revokeToken();">Revoke access token</button>

  <script>

    const gapiLoadPromise = new Promise((resolve, reject) => {
      gapiLoadOkay = resolve;
      gapiLoadFail = reject;
    });
    const gisLoadPromise = new Promise((resolve, reject) => {
      gisLoadOkay = resolve;
      gisLoadFail = reject;
    });

    var tokenClient;

    (async () => {
      document.getElementById("showEventsBtn").style.visibility="hidden";
      document.getElementById("revokeBtn").style.visibility="hidden";

      // First, load and initialize the gapi.client
      await gapiLoadPromise;
      await new Promise((resolve, reject) => {
        // NOTE: the 'auth2' module is no longer loaded.
        gapi.load('client', {callback: resolve, onerror: reject});
      });
      await gapi.client.init({
        // NOTE: OAuth2 'scope' and 'client_id' parameters have moved to initTokenClient().
      })
      .then(function() {  // Load the Calendar API discovery document.
        gapi.client.load('https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest');
      });

      // Now load the GIS client
      await gisLoadPromise;
      await new Promise((resolve, reject) => {
        try {
          tokenClient = google.accounts.oauth2.initTokenClient({
              client_id: 'YOUR_CLIENT_ID',
              scope: 'https://www.googleapis.com/auth/calendar.readonly',
              prompt: 'consent',
              callback: '',  // defined at request time in await/promise scope.
          });
          resolve();
        } catch (err) {
          reject(err);
        }
      });

      document.getElementById("showEventsBtn").style.visibility="visible";
      document.getElementById("revokeBtn").style.visibility="visible";
    })();

    async function getToken(err) {

      if (err.result.error.code == 401 || (err.result.error.code == 403) &&
          (err.result.error.status == "PERMISSION_DENIED")) {

        // The access token is missing, invalid, or expired, prompt for user consent to obtain one.
        await new Promise((resolve, reject) => {
          try {
            // Settle this promise in the response callback for requestAccessToken()
            tokenClient.callback = (resp) => {
              if (resp.error !== undefined) {
                reject(resp);
              }
              // GIS has automatically updated gapi.client with the newly issued access token.
              console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));
              resolve(resp);
            };
            tokenClient.requestAccessToken();
          } catch (err) {
            console.log(err)
          }
        });
      } else {
        // Errors unrelated to authorization: server errors, exceeding quota, bad requests, and so on.
        throw new Error(err);
      }
    }

    function showEvents() {

      // Try to fetch a list of Calendar events. If a valid access token is needed,
      // prompt to obtain one and then retry the original request.
      gapi.client.calendar.events.list({ 'calendarId': 'primary' })
      .then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
      .catch(err  => getToken(err))  // for authorization errors obtain an access token
      .then(retry => gapi.client.calendar.events.list({ 'calendarId': 'primary' }))
      .then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
      .catch(err  => console.log(err));   // cancelled by user, timeout, etc.
    }

    function revokeToken() {
      let cred = gapi.client.getToken();
      if (cred !== null) {
        google.accounts.oauth2.revoke(cred.access_token, () => {console.log('Revoked: ' + cred.access_token)});
        gapi.client.setToken('');
      }
    }

  </script>

  <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoadOkay()" onerror="gapiLoadFail(event)"></script>
  <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoadOkay()" onerror="gisLoadFail(event)"></script>

</body>
</html>
AGamePlayer
  • 7,404
  • 19
  • 62
  • 119
  • Have you found a way? The old Google Sign-In library (https://developers.google.com/identity/sign-in/web/sign-in) used to use gapi.auth to keep a session alive. Now that gapi.auth is deprecated, I don't know how to have gapi.client keep its session alive after a refresh. – lordofmax Oct 29 '22 at 08:09

2 Answers2

1

According to their sample (the callback one), you only need to

if (gapi.client.getToken() === null) {
  // Prompt the user to select an Google Account and asked for consent to share their data
  // when establishing a new session.
  tokenClient.requestAccessToken({
    prompt: 'consent'
  });
} else {
  // Skip display of account chooser and consent dialog for an existing session.
  tokenClient.requestAccessToken({
    prompt: ''
  });
}

In other words, change prompt: 'consent' to prompt: '' in this section of code:

tokenClient = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly',
  prompt: '',   // <------
  callback: '', // defined at request time in await/promise scope.
})
IT goldman
  • 14,885
  • 2
  • 14
  • 28
  • 1
    If I refresh the page, the `gapi.client.getToken()` will be reset to `null` every time, so I still have to see the consent again. – AGamePlayer Sep 06 '22 at 05:24
  • You can always go with `prompt: ''` regardless of condition of token. At worst it will ask you for prompt of user if you didn't sign in before. – IT goldman Sep 06 '22 at 08:14
1

After doing a bit of investigation into this issue, it appears that one of the causes of your issue could be the result of using localhost together with GAPI. There is not much documentation with localhost in Google Docs but a similar issue is observed here (some time back): https://stackoverflow.com/a/49619742/7677874.

My assumption is that even though Google allows testing its services on localhost, it does not trust localhost to persist data.

If your solution has to be run on localhost, there are ways to get around the persistence issue by passing login_hint to the GAPI, but that requires its own explanation.

My suggest would be to use something like NGROK as it offers a simple solution to quickly expose a local server to the Internet - https://ngrok.com/ Other tools can be found here: OAuth: how to test with local URLs?

Above tool will allow you to test persistence on a non-localhost domain.

If that fails, the best solution is to run your code behind a framework - that also helps with persistance - How to make the Google Drive API authorization persist when refreshing the page?