Google-api-php-client: How do you refresh access tokens?

Created on 2 Jul 2018  路  15Comments  路  Source: googleapis/google-api-php-client

At https://developers.google.com/api-client-library/php/auth/web-app#offline it says:

Access tokens periodically expire. You can refresh an access token without prompting the user for permission (including when the user is not present) if you requested offline access to the scopes associated with the token.

So, how is the developer supposed to do that? This looks like the answer:

If your application needs offline access to a Google API, set the API client's access type to offline:

$client->setAccessType("offline");

After a user grants offline access to the requested scopes, you can continue to use the API client to access Google APIs on the user's behalf when the user is offline. The client object will refresh the access token as needed.

So, the documentation is cristal-clear: apparently, you don't need to do anything, other than calling setAccessType("offline"). Apparently, the library is supposed to take care of refreshing the tokens automagically.

Well, nope.

I have tried the examples (which don't involve refreshing the tokens) and they worked. I tried storing the obtained tokens in a database and then using the token I had stored within the expiration time, and it worked.
But then I tried using the stored token after a longer time, and of course, I get the "invalid credentials" error because of the expired tokens.

Access tokens don't get automagically refreshed. If that is supposed to happen, it doesn't. More probably, there is something that we are supposed to do with the refresh_token item that is sometimes included in the responses returned by getAccessToken() (but not always, even if always configuring for offline access).

However, the docs don't mention about what it is that we should do in order to refresh a token.

triage me

Most helpful comment

Hey folks,
First, an apology: I've been on medical leave for a while and drowning in GitHub issues long before that. This slipped under my radar.

I've submitted an internal change to update the documentation here to include the prompt bits. The new sample code will look like this:

$client = new Google_Client();
$client->setAuthConfig('client_secret.json');
$client->addScope({{ oauth2_scope_php }});
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php');
// offline access will give you both an access and refresh token so that
// your app can refresh the access token without user interaction.
$client->setAccessType('offline');
// Using "force" ensures that your application always receives a refresh token.
// If you are not using offline access, you can omit this.
$client->setApprovalPrompt("force");
$client->setIncludeGrantedScopes(true);   // incremental auth

Expect the page to be updated in the new few days.

All 15 comments

Greetings! We'd be happy to help out, answer questions, and even fix bugs. I am however, going to have to ask you to monitor your language :) We're all humans here who would love to help, but please keep things respectful. We're working on getting a code of conduct in all of our repositories, but for now let this one be your guide. Thanks!

I'll try my best, it's difficult to hold back one's frustration. I don't think I have ever seen anything as poorly documented as Google's APIs and their PHP libraries.

Now, regarding the issue at hand, I have figured it out in detail.

The problem is that, when the user gets redirected back to the application after giving his consent, and the application calls

$client->authenticate($_GET['code'])

even if I ALWAYS set 'offline' access, the array returned by getAccessToken() sometimes contains a refresh_token element, sometimes it doesn't.
It seems that it only contains the refresh_token when the user grants the application access for the first time (i.e. has to click "allow"), or perhaps also when the user gets through the permissions screen a subsequent time (i.e. without having to click "allow" because he already authorized the app) if the last token has expired - but not in the other cases.

Now, to me that is plain wrong (and I'm under the impression this is the API's fault, not the library's, but I'm not 100% sure). But if this is the expected behavior _AND_ there's a good reason for this weird design, then it should definitely be documented and there should be examples.

The following workflow, which looks totally reasonable and seems to be pretty much the one suggested by the documentation (actually, the docs say very little about STORING access tokens, and in the examples there's NOTHING, so one can only infer), does not work:

  1. if you had previously stored an access token (the array returned by getAccessToken()), retrieve it and use it (i.e. setAccessToken())
  2. otherwise, redirect the user to Google's auth url
  3. when the user is redirected back to the app with a $_GET['code'], exchange the code for an access token, and store it.

This only works as long as:

  • the first time you send the user to Google's auth url, you _must_ save the token. You cannot afford to "miss" that opportunity, and then send the user to the auth url once more, and save the token on that occasion, because then you will not get the refresh token.
  • you must not send the user to Google's auth url if you already have a token (which includes a refresh_token). One would expect that if you send the user to the authentication page unnecessarily, nothing bad should happen (besides the minor annoyance for the user of going through an unnecessary redirect). But that is not the case. If you already have a valid token that is not expired and you do send the user to the authentication url, he'll come back with a code from which you'll obtain a token that does not contain the refresh_token. So, if you overwrite your stored token array with the fresh one, without inspecting its contents, you'll loose the refresh token.

So, you have 2 options:

  • A) you carefully observe the caveats above, don't miss the opportunity to save the token the very first time, and make sure to never send the user to the auth url if you already have a token that works
  • B) you take special care of storing the refresh_token, and when you exchange a code for a token, if it does not contain a refresh_token and you do have a refresh_token stored previously, you make sure not to delete the old refresh_token, which is still valid. That is, when you obtain an array from getAccessToken() (after calling authenticate()), you must not blindly store it overriding any previous token array you had previously stored. You must make sure that, if you had one that contained a refresh_token and the new one does not contain a refresh_token, the old refresh_token is kept.

Still, even in case B, which is more robust than A, you cannot afford to miss the opportunity to store the refresh_token the first time, because the next time you may not get it.

Now, you can't tell me that this is intuitive and that one is supposed to figure this out by reading the current documentation or the examples.

If authenticate() simply always fetched a refresh_token together with the access token (as long as the access type is offline), regardless of whether it's the first login or not, one wouldn't have to worry about any of this.

Hi @teo1978! I think the missing piece of the puzzle is that if you use access type offline and you've already gotten a refresh token at some point in the past, the authorization service will only give you an access token. You need to do prompt='consent' (which is documented somewhere in this long page).

And yes, in general you need to keep the access token given as long as possible - basically, keep it until it fails to get a new access token - then, mark it as invalid and prompt the user again.

I'm going to go ahead and close this, but if you need any more help please feel free to comment.

Hi @teo1978! I think the missing piece of the puzzle is that if you use access type offline and you've already gotten a refresh token at some point in the past, the authorization service will only give you an access token

Exactly, that's the missing piece of the puzzle.

So why do you close the issue without adding the missing piece to the documentation?

This behavior is far from obvious (indeed it's plain wrong, there's no valid reason why the authorization srvice shouldn't give you a refresh token every time - but I guess that's the API's fault, not the library's) and hence should be thoroughly documented.

Can documentation be contributed to? It is obvious it is left behind as it is still using authenticate instead of fetch... methods

Hey Google: closing this ticket without updating the documentation is VERY BAD. Do you care, even a little bit, about the programmers that are using your APIs? Then please give us BETTER documentation. Thanks.

Do you care, even a little bit, about the programmers that are using your APIs?

Nope, it's pretty obvious they don't.

@theacodes as per #issuecomment-401640922 please reopen.

Just joined the queue. :(

can you explain how you get them in the first place

@ib01 please open a new issue with your question, including as detailed a code sample as possible.

Here is a bit outdated but still very good example on configuring auth using PHP.
https://www.domsammut.com/code/php-server-side-youtube-v3-oauth-api-video-upload-guide

Also, to note there is a new function in Client.php you can use to refresh the access key if it's expired.

public function fetchAccessTokenWithRefreshToken($refreshToken = null)

Hey folks,
First, an apology: I've been on medical leave for a while and drowning in GitHub issues long before that. This slipped under my radar.

I've submitted an internal change to update the documentation here to include the prompt bits. The new sample code will look like this:

$client = new Google_Client();
$client->setAuthConfig('client_secret.json');
$client->addScope({{ oauth2_scope_php }});
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php');
// offline access will give you both an access and refresh token so that
// your app can refresh the access token without user interaction.
$client->setAccessType('offline');
// Using "force" ensures that your application always receives a refresh token.
// If you are not using offline access, you can omit this.
$client->setApprovalPrompt("force");
$client->setIncludeGrantedScopes(true);   // incremental auth

Expect the page to be updated in the new few days.

Thank you! Hope you are feeling better now :)

Guys I really can't believe the way this has been dealt with. It needs to be made really clear - the entire thing just falls over silently and the user has no idea how to resolve things.

If there is no refresh_token in the returned token then do NOT save or set the token. On the next authorization cycle it will be set - so basically if it is not set, do not save.

This needs to be made abundantly clear as there is a lot angst building towards google for the poor nature of documentation AND implementation of their API's.

Was this page helpful?
0 / 5 - 0 ratings