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

Google Refresh Token not always given back, intermittent #715

Open
sts-ryan-holton opened this issue Sep 16, 2024 · 6 comments
Open

Google Refresh Token not always given back, intermittent #715

sts-ryan-holton opened this issue Sep 16, 2024 · 6 comments

Comments

@sts-ryan-holton
Copy link

Socialite Version

5.16.0

Laravel Version

11.23.5

PHP Version

8.3.7

Database Driver & Version

MySQL 8

Description

When Google returns the user to the callback URL, the refreshToken field isn't always set and is sometimes null. This means that although I can store the token, it will expire after an hour and then no longer have a way to refresh it. I'm not sure why it's sometimes null, maybe there needs to be a way to revoke first? Or whether there's something else going on here.

Steps To Reproduce

  1. Redirect to Google using the following code:
Socialite::driver($service)->scopes([
    'https://www.googleapis.com/auth/calendar'
])->with(array_merge([
    'access_type' => 'offline'
], [
    'state' => 'integration_id='.$request->input('integration_id', '')
]))->redirect();
  1. Get the User from the callback:
$googleUser = Socialite::driver($service)->stateless()->user();

// outputting $googleUser?->refreshToken ?? null is sometimes null
Copy link

Thank you for reporting this issue!

As Laravel is an open source project, we rely on the community to help us diagnose and fix issues as it is not possible to research and fix every issue reported to us via GitHub.

If possible, please make a pull request fixing the issue you have described, along with corresponding tests. All pull requests are promptly reviewed by the Laravel team.

Thank you!

@ev-gor
Copy link

ev-gor commented Sep 18, 2024

Google returns a refresh token only when the consent screen is displayed and a user has granted the requested permissions to your app. All subsequent requests to the Google OAuth API do not provide a refresh token.
To bypass this limit, you can add 'prompt' => 'consent' to your array of params. This will redirect the user to the consent screen again, and Google will return a refresh token.

Socialite::driver('google')->with(array_merge([
        'access_type' => 'offline',
        'prompt' => 'consent',
    ], [
        'state' => 'integration_id='.$request->input('integration_id', '')
    ]))->redirect();

However, the official Google documentation includes a warning for such cases:

"There are limits on the number of refresh tokens that are issued: one limit per client/user combination, and another per user across all clients. If your application requests too many refresh tokens, it may run into these limits, in which case older refresh tokens stop working."

Therefore, request refresh tokens only when you really need them, and store them safely and permanently.

@sts-ryan-holton
Copy link
Author

sts-ryan-holton commented Sep 18, 2024

@ev-gor Thanks for that! Could you link me to the docs where you'd got that from please?

Does this mean then that I should do a check and if the returned user contains null for refresh token to just use the initial one stored in the database?

Additionally, the "expires in" fetched back is for the access token, not refresh token unless I'm mistaken?

Worth noting on this, I have a cron than runs every 15 minutes on my server that grabs all tokens which are due to expire based on the expires in, and it calls the refreshToken, here's what I've set it to:

/**
 * Refresh the token
 */
private function refreshToken(GoogleToken $token): void
{
    if (! $token->refresh_token) return;

    $newToken = Socialite::driver('google')->refreshToken($token?->refresh_token);

    if ($newToken?->token) {
        $token->update([
            'access_token' => $newToken?->token,
            'refresh_token' => $newToken?->refreshToken ?? ($token->refresh_token ?: null),
            'expires_in' => $newToken?->expiresIn ?? 0,
            'last_refresh_at' => now()
        ]);
    }
}```

@ev-gor
Copy link

ev-gor commented Sep 18, 2024

Google has very detailed documentation about its OAuth flow. For Laravel developers this article is especially useful.
The 'expires_in' field refers to an access token that lasts only about an hour. On the other hand, a refresh token has a very long lifespan, so while you have this token, you usually shouldn't worry about its renewal. However, there are several cases when a refresh token can expire. Keep these in mind when coding your authentication logic.

@sts-ryan-holton
Copy link
Author

@ev-gor So do you think, in the case of my project, that it's safe then to call Socialite::driver('google')->refreshToken($token?->refresh_token); each hour? My platform needs to make requests on the user's behalf, it's an "integration", so they won't be constantly re-authing. Just thinking, I've made some new changes to my code like adding:

$newToken?->refreshToken ?? ($token->refresh_token ?: null)

This should retain the existing token held in $token unless a new one is available, if neither are available then it sets it to null

@ev-gor
Copy link

ev-gor commented Sep 19, 2024

@sts-ryan-holton Sure, you can use the refreshToken method as often as you want with an existing refresh token. If this token becomes expired or invalid, Socialite will throw a GuzzleHttp\Exception\RequestException with "error": "invalid_grant" in its response body. You can use a try-catch block to check for the "invalid_grant" error. This will indicate that the refresh token is no longer valid, and the next time the user visits your app, you will redirect them to Google's consent screen to obtain another refresh token.

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

No branches or pull requests

3 participants