Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do Refresh Token results work, if done automatically within the API? #901

Closed
jonnywilliamson opened this issue Mar 18, 2016 · 14 comments · Fixed by googleapis/google-auth-library-php#110
Assignees

Comments

@jonnywilliamson
Copy link

Hi, I've no problem with understanding the refresh token, it's purpose, how to get it OR how to use it.

I am confused however about how the api uses it automatically when an access_token has expired. Here's a short example.

My Code:

  1. User approves access (with offline access)
  2. Refresh token and access token returned with time to expire (for access token)
  3. Refresh token saved to database for use.
  4. 1 day later, code retrieves access_token and expiry time from database. Detects that token should be expired. (uses isAccessTokenExpired())
  5. Manually use refresh token to get new access_token. Both saved/updated in database.
  6. New access_token used to get data from google service.

That's all fine.

However, I see in the code in Google_Client that the check to see if the access_token is expired is already called when the authorise method is called AND if it is expired and the refresh token has been supplied, it will use the refresh token when preparing the credentials.

This is where I get confused.

At no stage during the execution of the Google_Client code, can I find a place where the results of using the refresh_token can be 'intercepted' and thus, saved/updated to the users database.

I'd love to be able to remove all the time checking and isAccessTokenExpired() code from MY code and let the API deal with it (as it currently is). However, I DO want to know, if the API uses the refresh_token, what is the new access_token and when does it expire! I want to be able to update my database so that if I send a second request a few seconds later, it doesn't use the OLD access_token like it currently does in my codebase.

Can anyone shed somelight on what/how this works?

Many thanks.

@bshaffer
Copy link
Contributor

As long as the refresh_token key is set in the array passed to Google_Client::setAccessToken, the access token should be refreshed automatically.

If you're looking to save the access token somewhere, this is tricky, as it's not currently being saved back to the Google_Client. But it is stored in cache, so you could retrieve it using $client->getCache().

@jonnywilliamson
Copy link
Author

If you're looking to save the access token somewhere, this is tricky, as it's not currently being saved back to the Google_Client. But it is stored in cache, so you could retrieve it using $client->getCache().

Yes just so there's no confusion to my question:

I can see that the API is using the refresh_token automatically for me - also my request for data is successful so I know the API is working well. As you say, I just can't see how to get my hands on the new access_token and expires_in values etc from the request the API sent.

I haven't played around with the cache yet at all (I didn't even know there was one). I will do so now - however out of interest, when would the cache get updated with those new values? Immediately after the authorise() method or after the Google_Task_Runner run() method etc?

Just a little unfamiliar with the code.

Also just wondering how everyone else manages this as it would appear as a common scenario?

Am I doing it wrong? (Very common occurrence for me!!!)

Thank you for getting back to me.

@jonnywilliamson jonnywilliamson changed the title How does Refresh Token results work if done automatically? How do Refresh Token results work, if done automatically within the API? Mar 18, 2016
@bshaffer
Copy link
Contributor

Haha, no problem! No, you're not the only one to have this issue. In the downstream auth library, we just added the method getLastReceivedToken to the credentials classes. We have yet to expose this in Google_Client, but it would be nice to add a getLastReceivedToken to the client as well. This would be done by saving the $credentials object that was created, and then calling the method on that.

The access token is retrieved when GuzzleHttp issues an http request (so when Google_Task_Runner run() is executed). So something like this:

$service = new Google_Service_Drive($client);
$files = $service->files->list();

$token = $client->getCache()->get(sprintf('%s:%s', $clientId, implode(':', $scopes)));

It's not super elegant, but it should work.

@jonnywilliamson
Copy link
Author

Hi again,

Ok so having looked at the cache object I can see that there is indeed a key that holds the new access_token, that's a good start - thanks!

The key name is long and tricky (!!) using the clientId and scopes as you've shown above...however, there's only the new access_token and no expires_in data etc - like you would get back had we used the fetchAccessTokenWithRefreshToken method manually.

It feels like this might just be a feature in progress? Perhaps I should wait a while until this gets fleshed out a bit more with the Google_Client code?

Or have I read too much between the lines above and it's not really something you're planning on implementing...in which case, maybe I should put a request in. I'll have to read your etiquette rules too, don't want to appear rude and demanding :)

Thank you.

@jonnywilliamson
Copy link
Author

Just noticed this issue/request too..looks very similar. #300

@bshaffer
Copy link
Contributor

something like this might work:

class Google_Client
{
    // ...
    public function authorize(...)
    {
        // ...
        $this->setLastCredentials($credentials);
    }

    public function getLastReceivedToken()
    {
        if ($credentials = $this->getLastCredentials()) {
            return $credentials->getLastReceivedToken();
        }
    }
}

It's the easiest way to solve this problem, but it's not very elegant. A callback would work, or a better Cache API that allows you to chain caches (i.e. storages) so that access tokens can be persisted elsewhere)

@aaronlague
Copy link

Hi good day,

I have a question, is it possible to modify the expiry of the access_token? or of the session itself? or at least when the session expires, redirect or create a status message.

Sorry for barging in to this thread, it just so happen that I am having trouble with keeping the session for a longer time for the google api

thanks

@bshaffer
Copy link
Contributor

@jonnywilliamson PTAL at #905
@aaronlague it is not possible to modify the expiration of an access token. You can request additional tokens using refresh tokens or service accounts.

@jonnywilliamson
Copy link
Author

@bshaffer - Hey I'm only able to get looking at this today. Just about to check out the new code! Thanks!

@benkingcode
Copy link

benkingcode commented Dec 15, 2016

I've faced this same issue, and managed to get it working with this Laravel code:

$client->setTokenCallback(function ($cacheKey, $accessToken) use ($client) {
            $tokenToSave = Auth::user()->token;
            $tokenToSave['created'] = time();
            $tokenToSave['access_token'] = $accessToken;

            Auth::user()->update(['token' => $tokenToSave]);
        });

The problem is because the token callback only returns the access token, the rest of the token is no longer valid, particularly created and id_token. I've had to manually override created to stop the client from thinking it's permanently expired.

Is there any way of getting the full token back? Currently it appears that I can only get a valid id_token from the initial authorisation - any subsequent (automatic by the client) refreshes invalidated it and I have no way of getting it back.

@delmohf
Copy link

delmohf commented Dec 29, 2016

Probably I am missing something here but it was as simple as this when I tested some weeks ago:

$current_token = read_token_from_db(); //JSON as string
$client->setAccessToken($current_token);
//make some operation with Google services... then
$last_token = $client->getAccessToken();
if ($last_token != $current_token)
{
    write_token_to_db($last_token);
}

According to my tests $client->getAccessToken() will return the new access token if it was refreshed internally.

Is this not enough? Am I wrong?

@jonnywilliamson
Copy link
Author

Its been a while since I looked at this...I'll be digging into it again in a few weeks.

Perhaps it might be the case now that ->getAccessToken() returns the NEW token if it was created with the refresh token...however when this issue was first made, that was NOT how it worked.

@bshaffer
Copy link
Contributor

bshaffer commented Jan 4, 2017

@gsi-delmo I believe the issue is upon refreshing the token, the refresh token itself is not being persisted (see #1121)

@judgej
Copy link

judgej commented Jun 6, 2017

This is very interesting. None of the examples in the documentation tackle the problem of persisting the refreshed token at all. Instead, they keep using the initial token, which should include the refresh_token, over and over. That appears to work, but having put in a $client->setTokenCallback() as suggested by @dbbk I am seeing that the first request on every single page attempts to use the initial stored access token, finds it has expired, silently renews it using the initial stored refresh_token. Then on the next page, it goes through the whole process again. The access token is being renewed every single time the API is accessed (once per page).

That can't be good for network traffic! So the examples given seem to work beyond the 3600 seconds of the initial token being issued, but they are not working in the way people think they are.

When persisting the new access token, it needs to be provided in the same form as it was originally (access token, expiry time, refresh token etc.) since that is exactly what was persisted for the original authentication, is exactly what is given to the API client each time it is used, and is exactly where the API client gets the refresh token from when it needs it. So if it is being replaced in storage with a refreshed version, it needs to be complete enough, so that new one will work in another 3600 seconds when that expires.

Note: I am not manually renewing the token when its time has expired. I just letting the API client do that for me silently, which it will do (presumably on getting a 401) only if the access token it is given includes the refresh token in its structure.

Additional:

The API client auto-refresh seems to be time-based. Given the persisted access token, the created time needs to be updated too. This stores it in the session, as per many examples that store the original in the session with $_SESSION['token'] = $client->getAccessToken();:

$client->setTokenCallback(function ($cacheKey, $accessToken) use ($client) {
    // Persist the access token.
    $_SESSION['token']['access_token'] = $accessToken;
    // Without moving the created time on, the API client will refresh on every page load.
    $_SESSION['token']['created'] = time();
});

Now the question is what happens on the edge case? If the stored timestamp says the token has not expired, but Google with a server 5mS ahead of my clock says it has expired. Does the API client still silently refresh the token, or does it raise an exception? Getting the timing right to test this would be near impossible, so I guess I'm going to have to dive into the code. Edit: ah yes, I just wind the created time back after the token has expired. The API client cannot handle that - just raises an exception with a 401 error. That's a shame, because the token could expire at ANY time during ANY API call, and having it caught in the core API client so the token can be refreshed, followed by a retry of the API call, makes a lot of sense. There is really no reason the application using this library should have to handle it on every remote call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants