4

I have implemented the new Google Identity Services to get an access_token to call the Youtube API. I try to use this on an Angular app.

this.tokenClient = google.accounts.oauth2.initTokenClient({
  client_id: googleApiClientId,
  scope: 'https://www.googleapis.com/auth/youtube.readonly',
  callback: (tokenResponse) => {
    this.accessToken = tokenResponse.access_token;
  },
});

When I call this.tokenClient.requestAccessToken(), I can get an access token and use the Youtube API, that works.

But after one hour, this token expires. I have this error : "Request had invalid authentication credentials."

How can I get the newly refreshed access_token transparently for the user ?

Matt Sanders
  • 8,023
  • 3
  • 37
  • 49
Pretty Juice
  • 73
  • 1
  • 6

3 Answers3

2

There are two authorization flows for the Google Identity Services (GIS) library:

  1. The implicit flow, which is client-side only and uses .requestAccessToken()
  2. The authorization code flow, which requires a backend (server-side) as well and uses .requestCode()

With the implicit flow (which is what you are using), there are no refresh tokens. It is up to the client to detect tokens aging out and to re-run the token request flow. Here is some sample code from google's examples for how to handle this:

// initialize the client
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.
});

// handler for when token expires
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);
  }
}

// make the request
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.
}

Unfortunately GIS doesn't handle any of the token refreshing for you the way that GAPI did, so you will probably want to wrap your access in some common retry logic.

The important bits are that the status code will be a 401 or 403 and the status will be PERMISSION_DENIED.

You can see the details of this example here, toggle to the async/await tab to see the full code.

Matt Sanders
  • 8,023
  • 3
  • 37
  • 49
  • 3
    Thanks for this, clear as far as it goes. In my experience, calling `tokenClient.requestAccessToken()` again, results in the same UX for the user - the person is asked to again, interactively re-select the account they want to use. This is an unfortunate experience. Any hints on avoiding that? – Cheeso Nov 30 '22 at 18:02
  • 1
    @Cheeso - Yes, this is really challenging. There is more discussion about this in [this question](https://stackoverflow.com/questions/72080698/refresh-google-oauth2-token-automatically) that might be helpful. You can hint the user and use `prompt: ''` which makes the popup auto-select, but my current understanding is that to avoid it entirely you have to use a backend and go with the authorization code flow. If you find a better solution I'd love to hear about it. – Matt Sanders Nov 30 '22 at 18:34
  • In case this saves anyone time (took me a while to figure out), if you do migrate to the authorization code flow and you are using a popup to get the authorization code, you need to use `"postmessage"` as the `redirect_uri` for your authorization code -> tokens request. [More details here](https://stackoverflow.com/a/74655776/1006183). – Matt Sanders Dec 02 '22 at 12:37
1

To refresh the access token in a transparent way for the end-user you have to use the Refresh Token, This token will also come in the response to your call.

With this token, you can do a POST call to the URL: https://www.googleapis.com/oauth2/v4/token with the following request body

client_id: <YOUR_CLIENT_ID>
client_secret: <YOUR_CLIENT_SECRET>
refresh_token: <REFRESH_TOKEN_FOR_THE_USER>
grant_type: refresh_token

refresh token never expires so you can use it any number of times. The response will be a JSON like this:

{
  "access_token": "your refreshed access token",
  "expires_in": 3599,
  "scope": "Set of scope which you have given",
  "token_type": "Bearer"
}
  • 6
    Could you provide more details on how to do this? When using the `initTokenClient` method from the question, the response does not contain a `refresh_token` field, only `access_token`, `expires_in`, `scope` and `token_type`. – istvan.halmen Sep 01 '22 at 13:53
  • 1
    Having the same problem. The new library is not giving any hint on how to refresh user session silently. Calling requestAccessToken is showing popup – Ievgen Sep 29 '22 at 00:03
  • 1
    @levgen, did you resolved the issue? – Vishal Kiri Oct 01 '22 at 09:33
  • This answer is everywhere. But, how to get a refresh token? It is not returned from the initTokenClient method. That is the question on the internet that nobody answered. – Dhevendhiran M Oct 19 '22 at 12:52
  • Hi guys, do you guys found the answer to this? – Realizt30 Nov 01 '22 at 19:49
  • 1
    Hi https://stackoverflow.com/users/1841839/daimto I see that you're a google api expert and since you've marked my question as duplicate (https://stackoverflow.com/questions/74303317/how-to-manage-google-identity-service-access-token) , would you please give us a light here? – Realizt30 Nov 03 '22 at 17:56
0

@victor-navarro's answer is correct, but I think the URL is wrong. I made a POST call to https://oauth2.googleapis.com/token with a body like this and it worked for me:

client_id: <YOUR_CLIENT_ID>
client_secret: <YOUR_CLIENT_SECRET>
refresh_token: <REFRESH_TOKEN_FOR_THE_USER>
grant_type: refresh_token