I am trying to develop an application that will provide synchronization of folders between a Linux filesystem folder and Google Drive. The problem I have is that I cannot seem to get past a 403 error with the message:
Request had insufficient authentication scopes
I have success with the Google "Try the API" page here, when I populate these fields/values:
'corpa' => 'user'
'includeFilesFromAllDrives' => true
'supportsAllDrives' => true
'spaces' => 'drive'
That returns a list of objects. But when I try this in PHP, like:
$result = $drv->service->files->listFiles( [
'corpa' => 'user'
, 'includeFilesFromAllDrives' => true
, 'supportsAllDrives' => true
, 'spaces' => 'drive'
]) ;
the result is:
"error": {
"code": 403,
"message": "Request had insufficient authentication scopes.",
"errors": [
{
"message": "Insufficient Permission",
"domain": "global",
"reason": "insufficientPermissions"
}
],
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
"domain": "googleapis.com",
"metadata": {
"method": "google.apps.drive.v3.DriveFiles.List",
"service": "drive.googleapis.com"
}
}
]
}
}
On that page is a comment:
Requires one of the following OAuth scopes:
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.appdata
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.metadata
https://www.googleapis.com/auth/drive.metadata.readonly
https://www.googleapis.com/auth/drive.photos.readonly
https://www.googleapis.com/auth/drive.readonly
In an effort to get beyond this, I’ve actually selected all of these values (together) when creating the client. There isn’t any apparent objection to my use of the scopes, but still the authorization is rejected.
The code to build the $service object is in a class (DriveStuff), as follows:
public function GetService($clientSecretFile = null
, $scopes = null
, $useKey = false
, $debug = false
) {
if ($this->DEBUG)
error_log("GetService() called") ;
$this->loginEpoch = time() ; // For automatic login before key expiration (hard limit 1 hour)
if (is_null($this->firstLoginEpoch)) $this->firstLoginEpoch = $this->loginEpoch ;
$this->loginExpires = time() + (60 * 60) ; // Expires in 60 minutes
$this->loginCount++ ;
$pageFile = $this->pathToScript ;
if ($useKey) { // Using API KEY rather than login credentials
$client = new Google_Client() ;
$client->setApplicationName('gDrive stuff');
$client->setDeveloperKey($this->apiKey) ;
}
else { // Standard login to Client services
if (is_null($clientSecretFile))
$clientSecretFile = $this->clientFileJSON ;
if (is_null($clientSecretFile)) {
$clientSecretFile = $this->GetYouTubeControlParameter("CLIENT_SECRET_FILE", $pageFile) ;
$this->clientFileJSON = $clientSecretFile ;
}
if (is_null($scopes))
$scopes = $this->scopes ;
if (is_null($scopes)) {
$scopes = $this->GetYouTubeControlParameter("SCOPES", $pageFile) ;
$this->scopes = $scopes ;
}
if (is_string($scopes))
if (strstr($scopes, ',') > 0)
$scopes = explode(',', $scopes) ;
else
$scopes = [ $scopes ] ;
if ( ! in_array('https://www.googleapis.com/auth/drive', $scopes))
$scopes[] = 'https://www.googleapis.com/auth/drive' ; // Add this service
$this->scopes = $scopes ;
$client = $this->GetAuth($clientSecretFile, $scopes) ;
if ($debug) {
foreach ($client->getScopes() AS $scope)
error_log('Requesed scope: ' . $scope) ;
}
}
$service = new Google_Service_Drive($client) ;
$this->service = $service ;
$this->reLogInFile = $clientSecretFile ; // For automatic login before key expiration (hard limit 1 hour)
$this->reLogInScopes = $scopes ; // For automatic login before key expiration (hard limit 1 hour)
$this->reLogInUseKey = $useKey ; // For automatic login before key expiration (hard limit 1 hour)
return $this->service ;
}
The program code at this point is fairly straighrforward:
require_once('DriveRelated/DriveStuff.php') ;
$drv = new DriveStuff($lov, false) ; // True sets debug mode
$service = $drv->GetService(null
, [ 'https://www.googleapis.com/auth/drive'
, 'https://www.googleapis.com/auth/drive.appdata'
, 'https://www.googleapis.com/auth/drive.file'
, 'https://www.googleapis.com/auth/drive.metadata'
, 'https://www.googleapis.com/auth/drive.metadata.readonly'
, 'https://www.googleapis.com/auth/drive.photos.readonly'
, 'https://www.googleapis.com/auth/drive.readonly'
]
, false, true) ;
$result = $drv->service->files->listFiles( [ 'pageSize' => 3000 ],
[
'corpa' => 'user'
, 'includeFilesFromAllDrives' => true
, 'supportsAllDrives' => true
, 'spaces' => 'drive'
]) ;
… and the log file contains:
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive.appdata
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive.file
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive.metadata
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive.metadata.readonly
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive.photos.readonly
[03-Aug-2023 20:28:03 America/New_York] Requesed scope: https://www.googleapis.com/auth/drive.readonly]
[03-Aug-2023 20:28:05 America/New_York] PHP Fatal error: Uncaught GoogleServiceException: {
"error": {
"code": 403,
"message": "Request had insufficient authentication scopes.",
"errors": [
{
"message": "Insufficient Permission",
"domain": "global",
"reason": "insufficientPermissions"
}
],
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
"domain": "googleapis.com",
"metadata": {
"method": "google.apps.drive.v3.DriveFiles.List",
"service": "drive.googleapis.com"
}
}
]
}
}
in /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Http/REST.php:134
Stack trace:
#0 /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Http/REST.php(107): GoogleHttpREST::decodeHttpResponse()
#1 [internal function]: GoogleHttpREST::doExecute()
#2 /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Task/Runner.php(187): call_user_func_array()
#3 /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Http/REST.php(66): GoogleTaskRunner->run()
#4 /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Client.php(922): GoogleHttpREST::execute()
#5 /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Service/Resource.php(238): GoogleClient->execute()
#6 /www/cgi-bin/YouTubeRelated/vendor/google/apiclient-services/src/Drive/Resource/Files.php(258): GoogleServiceResource->call()
#7 /www/htdocs/gdrive/gdrive-test.php(30): GoogleServiceDriveResourceFiles->listFiles()
#8 {main}
thrown in /www/cgi-bin/YouTubeRelated/vendor/google/apiclient/src/Http/REST.php on line 134
By the way, I have tried a few subsets of the scopes (such as all readonly scopes, all-except-readonly, and so on), without any change in the result.
I have good, and rather extensive, experience with the YouTube API, so I am certain this will be overcome. Now I’m looking for the missing piece so I can proceed beyond this beginner stage with Google Drive.
Notice that the word ‘requested’ is misspelled, but this is only a cosmetic issue.
EDIT As requested, here is the authorizaion code:
/**
* Get access code from Database or from Google
* @param null|string %clientSecreFile json file from GoogleDegeloper's Console project export
* @param null|string|array $scopes the requested scope(s). If not specified, these will e retrieved from preferences
* @param string $appName value to place into client application name ($client->SetApplicationName())
* @return mixed client object
*/
public function GetAuth($clientSecretFile
, $scopes
, $appName = 'Google Drive stuff'
) {
$this->clientSecretFile = $clientSecretFile ;
if ($this->DEBUG)
error_log("GetAuth() called") ;
$client = new Google_Client();
$client_json = json_decode(file_get_contents($clientSecretFile), true) ;
// For Desktop apps, $client_json top will be 'installed'; for Weh apps, this is 'weh'....
if (count($client_json) == 1) // Key may be 'web' or 'installed'
$topKey = implode(array_keys($client_json)) ;
else
$topKey = 'installed' ;
$clientId = $client_json[$topKey]['client_id'] ;
$clientSecret = $client_json[$topKey]['client_secret'] ;
$client->setApplicationName($appName);
if ( is_string($scopes))
$scopes = [ $scopes ] ;
$client->setScopes($scopes);
$authHistoyRow['authConfig'] = json_encode($client_json) ;
$client->setAuthConfig($client_json) ;
$accessType = 'offline' ;
$client->setAccessType($accessType);
$this->GetClientFromDB($clientId // Handles inserttion of new Client information if necessary; returns as much information as we have otherwise
, $clientSecret
, $scope // Values below here (including this value) are passed by reference, and are updated by GetClientFromDB()
, $accessToken // Updated by GetClientFromDB()
, $tokenType // Updated by GetClientFromDB()
, $refreshToken // Updated by GetClientFromDB()
, $expires // Updated by GetClientFromDB()
, $created // Updated by GetClientFromDB()
, $rowId // Updated by GetClientFromDB()
) ;
$authHistoryRow = [] ;
$authHistoryRow['clientId'] = $clientId ;
$authHistoryRow['clientSecret'] = $clientSecret ;
$authHistoryRow['projectId'] = $client_json[$topKey]['project_id'] ;
$authHistoryRow['authUri'] = $client_json[$topKey]['auth_uri'] ;
$authHistoryRow['tokenUri'] = $client_json[$topKey]['token_uri'] ;
$authHistoryRow['authProviderX509CertUrl'] = $client_json[$topKey]['auth_provider_x509_cert_url'] ;
$authHistoryRow['appName'] = $appName ;
$authHistoryRow['scopes'] = implode(',', $scopes) ;
$authHistoryRow['accessType'] = $accessType ;
if ((! $rowId) || $client->isAccessTokenExpired()) {
$accessToken = null ;
// Refresh the token...
if ($rowId && $client->isAccessTokenExpired() && $refreshToken) {
$authHistoryRow['refreshToken'] = $refreshToken ;
$client->refreshToken($refreshToken) ;
$accessToken = $client->getAccessToken() ;
$authHistoryRow['accessToken'] = $accessToken ;
$expires = $accessToken['expires_in'] ;
$authHistoryRow['expires'] = $expires ;
}
if ((! $accessToken) ) { // Still not able to get authorization. Talk to user.
printf("Authorization expired at %sn", date('Y-m-d H:i', $expires)) ;
//$client->setRedirectUri('http://web.lovelady.com/google-api-php-client/examples/large-file-download.php') ;
$authUrl = $client->createAuthUrl();
printf("Open this link in your browser:n%sn", $authUrl);
printf('Enter verification code: ');
$authCode = trim(fgets(STDIN));
// Exchange authorization code for an access token.
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$output = print_r($accessToken, true) ;
file_put_contents("access_token.txt", $output) ;
}
$accessExpiresEpoch = time() + $accessToken['expires_in'] ;
$accessCreatedEpoch = $accessToken['created'] ;
$this->WriteClientToDB($clientId // UUpdate the database
, $clientSecret
, $accessToken['scope']
, $accessToken['access_token']
, $accessToken['token_type']
, $accessToken['refresh_token']
, $accessExpiresEpoch
, $accessCreatedEpoch
) ;
}
$client->setAccessToken($accessToken);
$authHistoryRow['accessToken'] = $accessToken ;
$this->addClientHistory($authHistoryRow) ;
return $client ;
} // End of GetAuth()
2
Answers
I think you might have an unverified application and therefore you have scopes that you cannot use. if your app is unverified you can only use the the nonsensitive scopes and not sensitive and restricted scopes.
I believe if you follow the guide below it should work with some caveats. unless you plan to use the oauth path to authorize your application.
https://developers.google.com/drive/api/guides/api-specific-auth
for @linda
if your public application uses scopes that permit access to certain user data, it must complete a verification process. If you see unverified app on the screen when testing your application, you must submit a verification request to remove it. Find out more about unverified apps and get answers to frequently asked questions about app verification in the Help Center.
table…
The Usage column in the table above indicates the sensitivity of each scope, according to the following definitions:
Recommended / Non-sensitive—These scopes provide the smallest scope of authorization access and only requires basic app verification. For information on this requirement, see Steps to prepare for verification.
Recommended / Sensitive—These scopes provide access to specific Google User Data that’s authorized by the user for your app. It requires you to go through additional app verification. For information on this requirement, see Steps for apps requesting sensitive scopes
Restricted—These scopes provide wide access to Google User Data and require you to go through a restricted scope verification process. For information on this requirement, see Google API Services: User Data Policy and Additional Requirements for Specific API Scopes. If you store restricted scope data on servers (or transmit), then you must go through a security assessment.
If your app requires access to any other Google APIs, you can add those scopes as well. For more information about Google API scopes, see Using OAuth 2.0 to Access Google APIs.
For more information about specific OAuth 2.0 scopes, see OAuth 2.0 Scopes for Google APIs.
I would like to propose that you delete thse fields
This should force your application to request authorization of the user again, at which time they should be shown the new scopes that your application is requesting access to and grant access. If your app is working correctly it will then store these new values into the database.
The issue you are having is that you have already requested access of the user and have stored a refresh token with the previous granted credentials. These are no good if you change the scopes that your app needs. You need to remove the previous refresh token and request a new one with the new scopes.