Kristian Vasilev
Kristian Vasilev

Reputation: 544

How does google-api work in offline request with tokens

My first problem is that when I make instance of the Google_Client and set all the needed scopes, configs, access types and others and then generate a URL for the user to AUTH with $client->createAuthUrl(); I allow access to my application with the google account that I'm logged. Everything works fine until I need to refresh the token.

I explain below about the token refresh.

  $this->client = new Google_Client();
            $this->client->setAuthConfig(Storage::path('secret.json'));
            $this->client->setAccessType("offline");        // offline access
            $this->client->setIncludeGrantedScopes(true);   // incremental auth
            $this->client->addScope(Google_Service_Drive::DRIVE_METADATA_READONLY);
            $this->client->setRedirectUri(env('APP_URL') . '/login/google/callback');

So the next thing I did is to get the google user data with laravel-socialite. This is the data I get:

The code for the request

  public function redirectToGoogleProvider()
    {
        $parameters = ['access_type' => 'offline'];

        return Socialite::driver('google')
            ->scopes(["https://www.googleapis.com/auth/drive"])
            ->with($parameters)
            ->redirect();
    }
User {#466 ▼
  +token: "ya29.GmPhBiSuYJ_oKsvDTUXrj3lJR5jlEYV4x8d0ZuYZLnDJgYL-uePT_KuUkIb-OQUiWBElhm6ljfac5QhDTXYK3qjWjje1vsnZsTAJ7pXyjNAVd2buZy0a6xZCTdnLdFNMKMDMXa6bGbA"
  +refreshToken: null
  +expiresIn: 3599
  +id: "101509757451285102354"
  +nickname: null
  +name: "My name"
  +email: "My email"
  +avatar: "https://lh3.googleusercontent.com/-Qsbr3VmcZ50/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rdS2HIl3LCv5EYCmvyXulG8n-Ap7w/mo/photo.jpg"
  +user: array:12 [▶]
  +"avatar_original": "https://lh3.googleusercontent.com/-Qsbr3VmcZ50/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rdS2HIl3LCv5EYCmvyXulG8n-Ap7w/mo/photo.jpg"
}

There is no refresh token here. I assume that this is because of the way that google works in offline mode

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 I set the token that I get from the request and store in the database to the Google_Client instance with

$client->setAccessToken(Auth::user()->getUserInfo()->refresh_token)

This works. But when the token needs to be refreshed It's not getting refreshed. Instead I'm getting

"Invalid creditianals error".

Google says that:

"The client object will refresh the access token as needed." But this is not happening.

I don't understand If I need to call a special method or how does the Google_Client handle the token refresh.

I've tried using the CODE parameter to fetch the token and refresh token with it.

$client->fetchAccessTokenWithAuthCode($code);

And got this error:

array:2 [▼
  "error" => "invalid_request"
  "error_description" => "Could not determine client ID from request."
]

I'm probably not doing something right but I have no clue what. I'm confused to be honest. Already lost a day on this without any success.

Upvotes: 3

Views: 2640

Answers (2)

Let me share what I have come up with finally at Laravel to make it work

public static function getOauth2Client($o_user, $f_client) {
    try {
        $o_client = new Google_Client();
        $o_client - >setAuthConfig($f_client);
        $o_client - >setAccessType('offline');
        $o_client - >setIncludeGrantedScopes(true);
        $i_token_remaining = ($o_user - >gdrive_access_ttl - (time() - $o_user - >gdrive_access_created));
        $google_client_token = ['refresh_token' = >$o_user - >gdrive_refresh_token, 'access_token' = >$o_user - >gdrive_access_token, 'created' = >$o_user - >gdrive_access_created, 'expires_in' = >$i_token_remaining, ];
        $o_client - >setAccessToken(json_encode($google_client_token));

        #token validity time is less than 60 seconds
        if ($i_token_remaining < 60 || $o_client - >isAccessTokenExpired()) {
            $res = $o_client - >fetchAccessTokenWithRefreshToken($o_client - >getRefreshToken());
            $o_user - >gdrive_access_token = $res['access_token'];
            $o_user - >gdrive_access_created = time();
            $o_user - >gdrive_access_ttl = $res['expires_in'];
        }
        return $o_client;
    } catch(Exception $e) {
        print 'An error occurred: '.$e - >getMessage();
    }
}

To my experience:

  • setAccessToken won't work with token only provided, it requires "expires_in" from me
  • $o_client->isAccessTokenExpired() always returns true unless "created" is provided with $google_client_token as "created" is taken into consideration for expiration
  • isAccessTokenExpired checks 30 seconds prior to expiration so my 60 seconds check can be safely removed
  • $o_user is user object e.g. User::find($i_user_id);
  • refresh token never changes, it is set once and forever, it is only access token that changes whenever refresh is requested
  • access token ttl is 3600 - guess why?)) - so please store and update access token as well
  • out of the scope of this function I also update my DB once access token changed
User::updateOrCreate(
    ['email' => $o_user - > email],
    [
        'gdrive_access_token' => $o_user - > gdrive_access_token,
        'gdrive_access_created' => $o_user - > gdrive_access_created,
        'gdrive_access_ttl' => $o_user - > gdrive_access_ttl,
    ]);

Upvotes: 0

Linda Lawton - DaImTo
Linda Lawton - DaImTo

Reputation: 117281

You will only get a refresh token back the first time you request access of the user. After that Google assumes that you saved the refresh token.

Have the user go to their google account and revoke the access granted to this client application.

Go to the page showing Apps with access to your account:

Then have them authenticate your application again. You should see the refresh toke at that time. Make sure you save it.

revoke

you may also be able to do

$client->revokeToken();

which should revoke the access the user granted. However i have not tried this to see if it gets me a new refresh token.

refresh access

This is my code for refreshing the access token NOTE its PHP i am not a larvel dev it looks very close you should be able to covert this if it doesnt work out of the box

/**
 * Authenticating to Google using Oauth2
 * Documentation:  https://developers.google.com/identity/protocols/OAuth2
 * Returns a Google client with refresh token and access tokens set. 
 *  If not authencated then we will redirect to request authencation.
 * @return A google client object.
 */
function getOauth2Client() {
    try {

        $client = buildClient();

        // Set the refresh token on the client. 
        if (isset($_SESSION['refresh_token']) && $_SESSION['refresh_token']) {
            $client->refreshToken($_SESSION['refresh_token']);
        }

        // If the user has already authorized this app then get an access token
        // else redirect to ask the user to authorize access to Google Analytics.
        if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {

            // Set the access token on the client.
            $client->setAccessToken($_SESSION['access_token']);                 

            // Refresh the access token if it's expired.
            if ($client->isAccessTokenExpired()) {              
                $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
                $client->setAccessToken($client->getAccessToken()); 
                $_SESSION['access_token'] = $client->getAccessToken();              
            }           
            return $client; 
        } else {
            // We do not have access request access.
            header('Location: ' . filter_var( $client->getRedirectUri(), FILTER_SANITIZE_URL));
        }
    } catch (Exception $e) {
        print "An error occurred: " . $e->getMessage();
    }
}

Upvotes: 2

Related Questions