google / php-photoslibrary

PHP client library for the Google Photos Library API
http://developers.google.com/photos
Apache License 2.0
90 stars 32 forks source link

Token has been expired or revoked #11

Closed ghost closed 5 years ago

ghost commented 5 years ago

Although I use the parameter "access_type = offline" the token expires and I can not work offline for more than 1 hour. The error message is: { "error": "invalid_grant", "error_description": "Token has been expired or revoked." }

And the code is $authCredentials = new UserRefreshCredentials( [ 'https://www.googleapis.com/auth/photoslibrary.sharing', 'https://www.googleapis.com/auth/photoslibrary' ], [ 'client_id' => $this->client_id, 'refresh_token' => $this->refreshToken, 'client_secret' => $this->clientSecret, ] ); How can I renew the token without user intervention?

ghost commented 5 years ago

Nobody is interested in this project

kryzhnii commented 5 years ago

It definitely somehow works both with access and refresh tokens, first one is temporary, the second one is used to obtain a new access token for all requests to API resources. Security thing.

I don't have time to understand all the workflow and how to use it fully with this lib. I think lib is able to take care of refreshing keys every hour, but we don't know how (documentation sucks).

Documentation says: use UserRefreshCredentials for initializing PhotosLibraryClient. And it really works with refresh token for every new album/photo/etc request, without using and without storing first obtained access_token, as in example project.

Two important things:

  1. Refresh token is accessible, but it is accessible only after the moment when user grants access to selected scope (first auth). It doesn't happening on the second or next times. If you can't see refresh token inside of oauth2 object, go Google profile setting and remove your app from auth list.
    $oauth2->setCode($code);
    $authToken = $oauth2->fetchAuthToken(); // this method obtains both tokens at first time 
    $refreshToken = $oauth2->getRefreshToken(); // not $authToken['access_token'];
    $credentials = new UserRefreshCredentials(
        $oauth2->getScope(),
        [
            'client_id' => $oauth2->getClientId(),
            'client_secret' => $oauth2->getClientSecret(),
            'refresh_token' => $refreshToken
        ]
    );
    // next, store $credentials somewhere and use for every PhotosLibraryClient initialization
  2. You may use prompt=consent for auth URL to force obtaining refresh token. (i.e. when you lost user's refresh token). In fact, you can use it every time:
    // user will be prompted to grant access to selected scope
    $authenticationUrl = $oauth2->buildFullAuthorizationUri(['access_type' => 'offline', 'prompt' => 'consent']); 
jfschmakeit commented 5 years ago

Authentication is handled by the Google Auth Library for PHP.

You can find out more about the different types of tokens in the general OAuth documentation - and in particular in the Server-side Web Apps guide. The section on refreshing an access token explains that you can use a refresh token to generate a new access token once it has expired.

However, the Google Auth Library for PHP should handle a lot of these things for you. (Note that the refresh token is only returned at the first grant for your application. You'll need to store it or request it again using the 'prompt=consent' prompt as called out above in https://github.com/google/php-photoslibrary/issues/11#issuecomment-467665634 by @chaosaround .)

bladx commented 5 years ago

Hello,

I have the exact same problem than @munix. My token expires if I keep the code as it is in the existing example.

So I tried the solution @chaosaround shared but then I got an unsupported_grant_type error. My code is here (I only edited the $refreshToken part and the prompt option):

function connectWithGooglePhotos(array $scopes, $redirectURI)
{
    $clientSecretJson = json_decode(
        file_get_contents('../client_secret.json'),
        true
    )['web'];
    $clientId = $clientSecretJson['client_id'];
    $clientSecret = $clientSecretJson['client_secret'];

    $oauth2 = new OAuth2([
        'clientId' => $clientId,
        'clientSecret' => $clientSecret,
        'authorizationUri' => 'https://accounts.google.com/o/oauth2/v2/auth',
        // Where to return the user to if they accept your request to access their account.
        // You must authorize this URI in the Google API Console.
        'redirectUri' => $redirectURI,
        'tokenCredentialUri' => 'https://www.googleapis.com/oauth2/v4/token',
        'scope' => $scopes,
    ]);

    // The authorization URI will, upon redirecting, return a parameter called code.
    if (!isset($_GET['code'])) {
        $authenticationUrl = $oauth2->buildFullAuthorizationUri(['access_type' => 'offline', 'prompt' => 'consent']);
        header("Location: " . $authenticationUrl);
    } else {
        // With the code returned by the OAuth flow, we can retrieve the refresh token.
        $oauth2->setCode($_GET['code']);
//        $authToken = $oauth2->fetchAuthToken();
//        $refreshToken = $authToken['access_token'];
        $refreshToken = $oauth2->getRefreshToken();

        // The UserRefreshCredentials will use the refresh token to 'refresh' the credentials when they expire.
        $_SESSION['credentials'] = new UserRefreshCredentials(
            $scopes,
            [
                'client_id' => $clientId,
                'client_secret' => $clientSecret,
                'refresh_token' => $refreshToken
            ]
        );

        // Return the user to the home page.
        header("Location: index.php");
    }
}

I use the v1.4.0 of google/photos-library.

I searched about this issue but did not find anything helpful. Hopefully someone might help me with this.

constracti commented 3 years ago

In my application, replacing

$refreshToken = $authToken['access_token'];

with

$refreshToken = $authToken['refresh_token'];

fixed the issue.

However, user consent should be always required:

$authorizationUri = $oauth2->buildFullAuthorizationUri( [
    'access_type' => 'offline',
    'prompt'      => 'consent',
] );

according to this stack overflow answer.