Google Drive API v3 - OAuth2 using Service Account in PHP/JWT

What?
This is an article explaining the code needed to write a PHP script which generates an access token for a service account which in turn is used to list files in a team's Google Drive.

This is very different to my code for OAuth when attended: Google Authentication - OAuth 2.0 using PHP/cURL... In that this one doesn't prompt for a user. Again this script doesn't need the client libraries, composer, vendor, etc.

Why?
This took me the best part of a month to get working. It is taken from Google's documentation as well as other forums and websites that try to explain it. Do not waste your time like I did on the public key, verifying a JWT signature, or including any third-party libraries. You can do it in pure PHP and all you need is the JSON key that you generated in your Google console.

Applies to:
  • Google Drive REST API v3
  • Google OAuth 2.0 v4
  • Google Cloud Platform IAM
  • Google Suite
  • PHP v5.6.35

How?
I'm going to go through each section of the code to go through the logic and highlight any changes you may need to make.

1. First: the variables are in arrays
Well mostly. Simply because we'll be working with JSON data and this encodes/decodes easily into PHP arrays. I can also output any of the variables and responses for debugging purposes. I can also unset multiple branches of variables with fewer commands than unsetting specific variables. Let's specify the output page to be in JSON format:
copyraw
// set content type of this page
header('Content-Type: application/json');

// init
$api = array();
$access_token = '';
  1.  // set content type of this page 
  2.  header('Content-Type: application/json')
  3.   
  4.  // init 
  5.  $api = array()
  6.  $access_token = ''

2a. Specify the location of your private key and token file
Here you need to enter the relative or absolute path to your private key, this should be the JSON file that you generated at Google's developer console - service accounts section. It also contains the public key under the guise of a URL (x509 certificate) but we don't need it for this script. Note that the key should not be stored in a public folder that is accessible via the web but at least stored (eg. outside your web root) where this script can access it.

The same goes for the access token, store it off the web but where this script can access it (read/write).
copyraw
// Location of private key on your server (JSON downloadable from Google)
$api['keys']['private']['file']         = '<relative_or_absolute_path_to_your_file_key>.json';  

// Location to store access token (needs be writeable)
$api['jwt']['token']['file']            = '<relative_or_absolute_path_to_your_file_token>/access_token.dat';
  1.  // Location of private key on your server (JSON downloadable from Google) 
  2.  $api['keys']['private']['file']         = '<relative_or_absolute_path_to_your_file_key>.json'
  3.   
  4.  // Location to store access token (needs be writeable) 
  5.  $api['jwt']['token']['file']            = '<relative_or_absolute_path_to_your_file_token>/access_token.dat'

2b. Specify the impersonator
Now for testing purposes you should leave this next variable as an empty string. If you leave this blank, the script will run through and connect to Google Drive using the Service Account. The script will work without being authorized, it just won't see any files other than its own.

If you specify an impersonator and you have NOT authorized this service account via the G-Suite Administrator settings, then this script will break. Once you have authorized the service account, you can then put the email of the user the service account will be impersonating.
  1. Browse to https://admin.google.com
  2. Go to Security > Show More > Advanced Settings > Manage API Client Access
  3. Enter the Client ID in the field Client Name (eg. 1000389324798977991)
  4. Enter the scope URL in the field One or More API Scopes (eg. https://www.googleapis.com/auth/drive)
  5. Click Authorize
copyraw
$api['gdrive']['impersonator']          = "";
  1.  $api['gdrive']['impersonator']          = ""

3. Google Endpoints
Only change these if you need a different scope or if the APIs get upgraded.
copyraw
$api['gapis']['oauth']['grant_type']    = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
$api['gapis']['oauth']['token']         = 'https://www.googleapis.com/oauth2/v4/token';
$api['gapis']['drive']['scope']         = 'https://www.googleapis.com/auth/drive';
$api['gapis']['drive']['files']         = 'https://www.googleapis.com/drive/v3/files';
  1.  $api['gapis']['oauth']['grant_type']    = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
  2.  $api['gapis']['oauth']['token']         = 'https://www.googleapis.com/oauth2/v4/token'
  3.  $api['gapis']['drive']['scope']         = 'https://www.googleapis.com/auth/drive'
  4.  $api['gapis']['drive']['files']         = 'https://www.googleapis.com/drive/v3/files'

4. Declare a PHP function to send requests using cURL
A standard function that is skipping the SSL checks and returns a PHP array
copyraw
function send_request($url, $header, $data, $method="GET") {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);
    $output = json_decode($response, true);
    return $output;
}
  1.  function send_request($url, $header, $data, $method="GET") { 
  2.      $ch = curl_init()
  3.      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false)
  4.      curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false)
  5.      curl_setopt($ch, CURLOPT_URL, $url)
  6.      curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method)
  7.      curl_setopt($ch, CURLOPT_HTTPHEADER, $header)
  8.      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data))
  9.      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true)
  10.      $response = curl_exec($ch)
  11.      curl_close($ch)
  12.      $output = json_decode($response, true)
  13.      return $output
  14.  } 

5. Get Minutes Remaining on cached token
So usually an access token will last for 60 minutes, in this example, we are going to store the token value so it is reused until it is about to expire. Firstly, we need to check how many minutes are remaining since the access token file was last modified:
copyraw
// check minutes remaining
$api['jwt']['token']['minutes']=0;
if(file_exists($api['jwt']['token']['file'])){
    $expiry_time = filemtime($api['jwt']['token']['file']) + 3600;
    $diff = $expiry_time - time();    
    $api['jwt']['token']['minutes'] = floor($diff/60);            
}
  1.  // check minutes remaining 
  2.  $api['jwt']['token']['minutes']=0
  3.  if(file_exists($api['jwt']['token']['file'])){ 
  4.      $expiry_time = filemtime($api['jwt']['token']['file']) + 3600
  5.      $diff = $expiry_time - time()
  6.      $api['jwt']['token']['minutes'] = floor($diff/60)
  7.  } 

6a. Use existing Access Token
This if else statement simply says if the token still has at least 5 minutes left, then use the one found at the file location specified earlier.
copyraw
if( $api['jwt']['token']['minutes'] > 5){
    $access_token = base64_decode(file_get_contents($api['jwt']['token']['file']));
    
}else{
  1.  if( $api['jwt']['token']['minutes'] > 5){ 
  2.      $access_token = base64_decode(file_get_contents($api['jwt']['token']['file']))
  3.   
  4.  }else{ 
Otherwise, let's generate a new access token.

6b. Get the contents of Google's JSON Key
This key doesn't just contain the private key we need. It is a JSON file with links to the public key, as well as info as to the project ID, the client ID, the client Email... Stuff that this script will use so:
copyraw
// Get JSON file (generated by Google) contents
    $api['keys']['private']['contents']    = json_decode( file_get_contents( $api['keys']['private']['file'] ), true);
  1.  // Get JSON file (generated by Google) contents 
  2.      $api['keys']['private']['contents']    = json_decode( file_get_contents( $api['keys']['private']['file'] ), true)

6c. Generate a JSON Web Token (JWT)
Now we need to generate the infamous JWT. If you've been trying to check your base64 encoded strings at JWT.io then it's hard because the timestamps, included in the encoding, change every second. You can get it verifying the signature successfully if you go get your public key, paste both keys into jwt.io (these don't change), then paste the encoded assertion.

Anyway, this is how you generate the JWT header
copyraw
// Build token header.  Specify algorithm
    $api['jwt']['header']['alg']           = 'RS256';
    $api['jwt']['header']['typ']           = 'JWT';
  1.  // Build token header.  Specify algorithm 
  2.      $api['jwt']['header']['alg']           = 'RS256'
  3.      $api['jwt']['header']['typ']           = 'JWT'
The JWT payload (or claim set as Google seems to refer to it as) has most of the ever-changing data. Note how the impersonator is only added if it is not blank. It must not be included in the claim set if you have not yet had the service account authorized by a G-Suite Administrator (see 2b). Scope is a string of scope urls delimited by a space (in this example only one). ISS is the client email taken from the JSON key downloaded from Google.
copyraw
// Build token payload for a JSON string
    $api['jwt']['claim_set']['iss']        = $api['keys']['private']['contents']['client_email'];
    if($api['gdrive']['impersonator']!=""){
        $api['jwt']['claim_set']['sub']    = $api['gdrive']['impersonator'];  // only if service account has been authorized
    }
    $api['jwt']['claim_set']['scope']      = $api['gapis']['drive']['scope'];
    $api['jwt']['claim_set']['aud']        = $api['gapis']['oauth']['token'];
    $api['jwt']['claim_set']['exp']        = strtotime('+1 hour');
    $api['jwt']['claim_set']['iat']        = strtotime('now');
  1.  // Build token payload for a JSON string 
  2.      $api['jwt']['claim_set']['iss']        = $api['keys']['private']['contents']['client_email']
  3.      if($api['gdrive']['impersonator']!=""){ 
  4.          $api['jwt']['claim_set']['sub']    = $api['gdrive']['impersonator'];  // only if service account has been authorized 
  5.      } 
  6.      $api['jwt']['claim_set']['scope']      = $api['gapis']['drive']['scope']
  7.      $api['jwt']['claim_set']['aud']        = $api['gapis']['oauth']['token']
  8.      $api['jwt']['claim_set']['exp']        = strtotime('+1 hour')
  9.      $api['jwt']['claim_set']['iat']        = strtotime('now')
And finally the JWT signature (or assertion in this case resulting as a combination of the header, data and signature): Using the JWT header, and claim set above, along with the private key, we'll openssl sign this with sha256. The output will be a full JWT: base64 encoded, URL-safe, three part string delimited by a period/dot:
copyraw
// Generate assertion/signature of JWT
    $api['jwt']['assertion']               = rtrim(strtr(base64_encode(json_encode($api['jwt']['header'])), '+/', '-_'), '=');
    $api['jwt']['assertion']              .= ".".rtrim(strtr(base64_encode(json_encode($api['jwt']['claim_set'])), '+/', '-_'), '=');
    $result = openssl_sign(
        $api['jwt']['assertion'], 
        $signature, 
        openssl_pkey_get_private($api['keys']['private']['contents']['private_key']), 
        'sha256');
    if ($result === true){
        $api['jwt']['assertion']          .= "." . rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
    }
  1.  // Generate assertion/signature of JWT 
  2.      $api['jwt']['assertion']               = rtrim(strtr(base64_encode(json_encode($api['jwt']['header'])), '+/', '-_'), '=')
  3.      $api['jwt']['assertion']              .= ".".rtrim(strtr(base64_encode(json_encode($api['jwt']['claim_set'])), '+/', '-_'), '=')
  4.      $result = openssl_sign( 
  5.          $api['jwt']['assertion'], 
  6.          $signature, 
  7.          openssl_pkey_get_private($api['keys']['private']['contents']['private_key']), 
  8.          'sha256')
  9.      if ($result === true){ 
  10.          $api['jwt']['assertion']          .= "." . rtrim(strtr(base64_encode($signature), '+/', '-_'), '=')
  11.      } 

7. Generate an Access Token
With the encoded JWT that we just generated, we can send a request to the Google OAuth endpoint for a new token:
copyraw
// prepare token request
    $url                                   = $api['gapis']['oauth']['token'];
    $header[]                              = 'Content-Type: application/x-www-form-urlencoded';
    $data['grant_type']                    = $api['gapis']['oauth']['grant_type'];
    $data['assertion']                     = $api['jwt']['assertion'];

    // send request
    $api['jwt']['token']['response']       = send_request($url, $header, $data, "POST");

    // check if response exists
    if(isset($api['jwt']['token']['response']['access_token'])){

        // store in var
        $access_token = $api['jwt']['token']['response']['access_token'];

        // store in file
        file_put_contents($api['jwt']['token']['file'], base64_encode($access_token));
    
        // reset minutes counter
        $api['jwt']['token']['minutes'] = 59;
    
    }
} // end if( $api['jwt']['token']['minutes'] > 5
  1.  // prepare token request 
  2.      $url                                   = $api['gapis']['oauth']['token']
  3.      $header[]                              = 'Content-Type: application/x-www-form-urlencoded'
  4.      $data['grant_type']                    = $api['gapis']['oauth']['grant_type']
  5.      $data['assertion']                     = $api['jwt']['assertion']
  6.   
  7.      // send request 
  8.      $api['jwt']['token']['response']       = send_request($url, $header, $data, "POST")
  9.   
  10.      // check if response exists 
  11.      if(isset($api['jwt']['token']['response']['access_token'])){ 
  12.   
  13.          // store in var 
  14.          $access_token = $api['jwt']['token']['response']['access_token']
  15.   
  16.          // store in file 
  17.          file_put_contents($api['jwt']['token']['file'], base64_encode($access_token))
  18.   
  19.          // reset minutes counter 
  20.          $api['jwt']['token']['minutes'] = 59
  21.   
  22.      } 
  23.  } // end if( $api['jwt']['token']['minutes'] > 5 
Close off this if else statement (if time remaining > 5 minutes) here if you are not using the full script below.

8. Done
That's it! You have an access token, your service account can connect directly with its Google Drive. To get a file listing, try the following:
copyraw
// Get File List
$url            = $api['gapis']['drive']['files'];
$header[]       = 'Content-Type: application/x-www-form-urlencoded';
$header[]       = 'Authorization: Bearer '.$access_token;
$data           = array();
$api['gdrive']  = send_request($url, $header, $data, "GET");

// Output JSON
echo json_encode($api, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  1.  // Get File List 
  2.  $url            = $api['gapis']['drive']['files']
  3.  $header[]       = 'Content-Type: application/x-www-form-urlencoded'
  4.  $header[]       = 'Authorization: Bearer '.$access_token
  5.  $data           = array()
  6.  $api['gdrive']  = send_request($url, $header, $data, "GET")
  7.   
  8.  // Output JSON 
  9.  echo json_encode($api, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)

The full script:
Just make the changes to the first few variables, as per the above instructions, to configure it...
copyraw
<?php
/*
	------------------------------------------------------------------------------------------------
	Google Drive REST API v3 using a Service Account
	------------------------------------------------------------------------------------------------

    Service Account Details issued by Google Cloud Platform IAM
                    https://console.developers.google.com/iam-admin/serviceaccounts

    Service Account Authorization by G-Suite Administrator
                    https://admin.google.com

    Google Drive API v3:
                    https://developers.google.com/drive/api/v3/reference

    Google OAuth 2.0 Playground:
                    https://developers.google.com/oauthplayground/

    Google Scopes
                    https://developers.google.com/identity/protocols/googlescopes
*/

// set content type of this page
header('Content-Type: application/json');

// init
$api = array();
$access_token = '';


// *************************************************************************************************
// EDIT THE FOLLOWING 
// REMINDER: Do not store key in publicly accessible web-folder but where this script can access it.

// Location of private key on your server (JSON downloadable from Google)
$api['keys']['private']['file']         = '<relative_or_absolute_path_to_your_file_key>.json';  

// Location to store access token (needs be writeable)
$api['jwt']['token']['file']            = '<relative_or_absolute_path_to_your_file_token>/access_token.dat';  

// Email of the user to impersonate (leave blank until authorized)
// IMPORTANT: An admin of the GSuite domain has to authorize this client id with the GDrive scope.
//  1. Browse to https://admin.google.com
//  2. Go to Security > Show More > Advanced Settings > Manage API Client Access
//  3. Enter the Client ID in the field "Client Name"
//  4. Enter the scope URL in the field "One or More API Scopes" (eg. https://www.googleapis.com/auth/drive)
//  5. Click "Authorize"
$api['gdrive']['impersonator']          = "";  

// Google Defaults (only change if the API needs upgrading)
$api['gapis']['oauth']['grant_type']    = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
$api['gapis']['oauth']['token']         = 'https://www.googleapis.com/oauth2/v4/token';
$api['gapis']['drive']['scope']         = 'https://www.googleapis.com/auth/drive';
$api['gapis']['drive']['files']         = 'https://www.googleapis.com/drive/v3/files';


// *************************************************************************************************
// FUNCTION

// function using PHP & cURL to send requests. Returns array
function send_request($url, $header, $data, $method="GET") {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);
    $output = json_decode($response, true);
    return $output;
}


// *************************************************************************************************
// GENERATE OAUTH ACCESS TOKEN
// only generate another if stored token will expire soon

// check minutes remaining
$api['jwt']['token']['minutes']=0;
if(file_exists($api['jwt']['token']['file'])){
    $expiry_time = filemtime($api['jwt']['token']['file']) + 3600;
    $diff = $expiry_time - time();    
    $api['jwt']['token']['minutes'] = floor($diff/60);            
}

// if at least 5 minutes, then use stored token
if( $api['jwt']['token']['minutes'] > 5){
    $access_token = base64_decode(file_get_contents($api['jwt']['token']['file']));
    
}else{

    // Get JSON file (generated by Google) contents
    $api['keys']['private']['contents']    = json_decode( file_get_contents( $api['keys']['private']['file'] ), true);

    // Build token header.  Specify algorithm
    $api['jwt']['header']['alg']           = 'RS256';
    $api['jwt']['header']['typ']           = 'JWT';

    // Build token payload for a JSON string
    $api['jwt']['claim_set']['iss']        = $api['keys']['private']['contents']['client_email'];
    if($api['gdrive']['impersonator']!=""){
        $api['jwt']['claim_set']['sub']    = $api['gdrive']['impersonator'];  // only if service account has been authorized
    }
    $api['jwt']['claim_set']['scope']      = $api['gapis']['drive']['scope'];
    $api['jwt']['claim_set']['aud']        = $api['gapis']['oauth']['token'];
    $api['jwt']['claim_set']['exp']        = strtotime('+1 hour');
    $api['jwt']['claim_set']['iat']        = strtotime('now');

    // Generate assertion/signature of JWT
    $api['jwt']['assertion']               = rtrim(strtr(base64_encode(json_encode($api['jwt']['header'])), '+/', '-_'), '=');
    $api['jwt']['assertion']              .= ".".rtrim(strtr(base64_encode(json_encode($api['jwt']['claim_set'])), '+/', '-_'), '=');
    $result = openssl_sign(
        $api['jwt']['assertion'], 
        $signature, 
        openssl_pkey_get_private($api['keys']['private']['contents']['private_key']), 
        'sha256');
    if ($result === true){
        $api['jwt']['assertion']          .= "." . rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
    }

    // prepare token request
    $url                                   = $api['gapis']['oauth']['token'];
    $header[]                              = 'Content-Type: application/x-www-form-urlencoded';
    $data['grant_type']                    = $api['gapis']['oauth']['grant_type'];
    $data['assertion']                     = $api['jwt']['assertion'];

    // send request
    $api['jwt']['token']['response']       = send_request($url, $header, $data, "POST");

    // check if response exists
    if(isset($api['jwt']['token']['response']['access_token'])){

        // store in var
        $access_token = $api['jwt']['token']['response']['access_token'];

        // store in file
        file_put_contents($api['jwt']['token']['file'], base64_encode($access_token));
    
        // reset minutes counter
        $api['jwt']['token']['minutes'] = 59;
    
    }
}

// no longer needed by this script
unset($api['keys']);
unset($api['jwt']);
unset($api['gapis']['oauth']);


// *************************************************************************************************
// Connect to GDrive and do stuff

// Get File List
$url            = $api['gapis']['drive']['files'];
$header[]       = 'Content-Type: application/x-www-form-urlencoded';
$header[]       = 'Authorization: Bearer '.$access_token;
$data           = array();
$api['gdrive']  = send_request($url, $header, $data, "GET");


// *************************************************************************************************
// OUTPUT

echo json_encode($api, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  1.  <?php 
  2.  /* 
  3.      ------------------------------------------------------------------------------------------------ 
  4.      Google Drive REST API v3 using a Service Account 
  5.      ------------------------------------------------------------------------------------------------ 
  6.   
  7.      Service Account Details issued by Google Cloud Platform IAM 
  8.                      https://console.developers.google.com/iam-admin/serviceaccounts 
  9.   
  10.      Service Account Authorization by G-Suite Administrator 
  11.                      https://admin.google.com 
  12.   
  13.      Google Drive API v3: 
  14.                      https://developers.google.com/drive/api/v3/reference 
  15.   
  16.      Google OAuth 2.0 Playground: 
  17.                      https://developers.google.com/oauthplayground/ 
  18.   
  19.      Google Scopes 
  20.                      https://developers.google.com/identity/protocols/googlescopes 
  21.  */ 
  22.   
  23.  // set content type of this page 
  24.  header('Content-Type: application/json')
  25.   
  26.  // init 
  27.  $api = array()
  28.  $access_token = ''
  29.   
  30.   
  31.  // ************************************************************************************************* 
  32.  // EDIT THE FOLLOWING 
  33.  // REMINDER: Do not store key in publicly accessible web-folder but where this script can access it. 
  34.   
  35.  // Location of private key on your server (JSON downloadable from Google) 
  36.  $api['keys']['private']['file']         = '<relative_or_absolute_path_to_your_file_key>.json'
  37.   
  38.  // Location to store access token (needs be writeable) 
  39.  $api['jwt']['token']['file']            = '<relative_or_absolute_path_to_your_file_token>/access_token.dat'
  40.   
  41.  // Email of the user to impersonate (leave blank until authorized) 
  42.  // IMPORTANT: An admin of the GSuite domain has to authorize this client id with the GDrive scope. 
  43.  //  1. Browse to https://admin.google.com 
  44.  //  2. Go to Security > Show More > Advanced Settings > Manage API Client Access 
  45.  //  3. Enter the Client ID in the field "Client Name" 
  46.  //  4. Enter the scope URL in the field "One or More API Scopes" (eg. https://www.googleapis.com/auth/drive) 
  47.  //  5. Click "Authorize" 
  48.  $api['gdrive']['impersonator']          = ""
  49.   
  50.  // Google Defaults (only change if the API needs upgrading) 
  51.  $api['gapis']['oauth']['grant_type']    = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
  52.  $api['gapis']['oauth']['token']         = 'https://www.googleapis.com/oauth2/v4/token'
  53.  $api['gapis']['drive']['scope']         = 'https://www.googleapis.com/auth/drive'
  54.  $api['gapis']['drive']['files']         = 'https://www.googleapis.com/drive/v3/files'
  55.   
  56.   
  57.  // ************************************************************************************************* 
  58.  // FUNCTION 
  59.   
  60.  // function using PHP & cURL to send requests. Returns array 
  61.  function send_request($url, $header, $data, $method="GET") { 
  62.      $ch = curl_init()
  63.      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false)
  64.      curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false)
  65.      curl_setopt($ch, CURLOPT_URL, $url)
  66.      curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method)
  67.      curl_setopt($ch, CURLOPT_HTTPHEADER, $header)
  68.      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data))
  69.      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true)
  70.      $response = curl_exec($ch)
  71.      curl_close($ch)
  72.      $output = json_decode($response, true)
  73.      return $output
  74.  } 
  75.   
  76.   
  77.  // ************************************************************************************************* 
  78.  // GENERATE OAUTH ACCESS TOKEN 
  79.  // only generate another if stored token will expire soon 
  80.   
  81.  // check minutes remaining 
  82.  $api['jwt']['token']['minutes']=0
  83.  if(file_exists($api['jwt']['token']['file'])){ 
  84.      $expiry_time = filemtime($api['jwt']['token']['file']) + 3600
  85.      $diff = $expiry_time - time()
  86.      $api['jwt']['token']['minutes'] = floor($diff/60)
  87.  } 
  88.   
  89.  // if at least 5 minutes, then use stored token 
  90.  if( $api['jwt']['token']['minutes'] > 5){ 
  91.      $access_token = base64_decode(file_get_contents($api['jwt']['token']['file']))
  92.   
  93.  }else{ 
  94.   
  95.      // Get JSON file (generated by Google) contents 
  96.      $api['keys']['private']['contents']    = json_decode( file_get_contents( $api['keys']['private']['file'] ), true)
  97.   
  98.      // Build token header.  Specify algorithm 
  99.      $api['jwt']['header']['alg']           = 'RS256'
  100.      $api['jwt']['header']['typ']           = 'JWT'
  101.   
  102.      // Build token payload for a JSON string 
  103.      $api['jwt']['claim_set']['iss']        = $api['keys']['private']['contents']['client_email']
  104.      if($api['gdrive']['impersonator']!=""){ 
  105.          $api['jwt']['claim_set']['sub']    = $api['gdrive']['impersonator'];  // only if service account has been authorized 
  106.      } 
  107.      $api['jwt']['claim_set']['scope']      = $api['gapis']['drive']['scope']
  108.      $api['jwt']['claim_set']['aud']        = $api['gapis']['oauth']['token']
  109.      $api['jwt']['claim_set']['exp']        = strtotime('+1 hour')
  110.      $api['jwt']['claim_set']['iat']        = strtotime('now')
  111.   
  112.      // Generate assertion/signature of JWT 
  113.      $api['jwt']['assertion']               = rtrim(strtr(base64_encode(json_encode($api['jwt']['header'])), '+/', '-_'), '=')
  114.      $api['jwt']['assertion']              .= ".".rtrim(strtr(base64_encode(json_encode($api['jwt']['claim_set'])), '+/', '-_'), '=')
  115.      $result = openssl_sign( 
  116.          $api['jwt']['assertion'], 
  117.          $signature, 
  118.          openssl_pkey_get_private($api['keys']['private']['contents']['private_key']), 
  119.          'sha256')
  120.      if ($result === true){ 
  121.          $api['jwt']['assertion']          .= "." . rtrim(strtr(base64_encode($signature), '+/', '-_'), '=')
  122.      } 
  123.   
  124.      // prepare token request 
  125.      $url                                   = $api['gapis']['oauth']['token']
  126.      $header[]                              = 'Content-Type: application/x-www-form-urlencoded'
  127.      $data['grant_type']                    = $api['gapis']['oauth']['grant_type']
  128.      $data['assertion']                     = $api['jwt']['assertion']
  129.   
  130.      // send request 
  131.      $api['jwt']['token']['response']       = send_request($url, $header, $data, "POST")
  132.   
  133.      // check if response exists 
  134.      if(isset($api['jwt']['token']['response']['access_token'])){ 
  135.   
  136.          // store in var 
  137.          $access_token = $api['jwt']['token']['response']['access_token']
  138.   
  139.          // store in file 
  140.          file_put_contents($api['jwt']['token']['file'], base64_encode($access_token))
  141.   
  142.          // reset minutes counter 
  143.          $api['jwt']['token']['minutes'] = 59
  144.   
  145.      } 
  146.  } 
  147.   
  148.  // no longer needed by this script 
  149.  unset($api['keys'])
  150.  unset($api['jwt'])
  151.  unset($api['gapis']['oauth'])
  152.   
  153.   
  154.  // ************************************************************************************************* 
  155.  // Connect to GDrive and do stuff 
  156.   
  157.  // Get File List 
  158.  $url            = $api['gapis']['drive']['files']
  159.  $header[]       = 'Content-Type: application/x-www-form-urlencoded'
  160.  $header[]       = 'Authorization: Bearer '.$access_token
  161.  $data           = array()
  162.  $api['gdrive']  = send_request($url, $header, $data, "GET")
  163.   
  164.   
  165.  // ************************************************************************************************* 
  166.  // OUTPUT 
  167.   
  168.  echo json_encode($api, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)

Conclusions
I spent way too much time looking at libraries to encrypt using an RS256 algorithm, too much time searching for the public key and wondering why JWT.io would always invalidate my signature... The above code doesn't look like much out there on the web because nothing on the web (that I could find) works the way this script does. A testament to how the official documentation was misleading. It is written from scratch by following the logic of many other scripts rather than a clear example from start to finish.


Google Drive File Listing
The example script above will list all files the service account can see. If you want to be a bit more specific to the listing of files (by using filters), I use the following code which searches by name, folder and not trashed respectively (change the name and google folder ID as per your own configuration):
copyraw
// build up query
$q[]                        = "name='my_file.avi'";                             // specify name of file to find (with extension)
$q[]                        = "'uhIqdg8k9DcLY2p2D0A7wIRGrhg0kU2' in parents";   // specify target google folder ID here
$q[]                        = "trashed=false";                                  // display items not trashed
$api['gdrive']['query']     = str_replace(' ', '+', implode(' and ', $q));      // join clauses with ' and ' and replace spaces with pluses

// send request to find a file in this folder (checking if file already exists)
$url                        = 'https://www.googleapis.com/drive/v3/files?q='. $api['gdrive']['query'];
$header[]                   = 'Content-Type: application/x-www-form-urlencoded';
$header[]                   = 'Authorization: Bearer '.$access_token;
$data                       = array();
$api['gdrive']['listing']   = send_request($url, $header, $data, "GET");
  1.  // build up query 
  2.  $q[]                        = "name='my_file.avi'";                             // specify name of file to find (with extension) 
  3.  $q[]                        = "'uhIqdg8k9DcLY2p2D0A7wIRGrhg0kU2' in parents";   // specify target google folder ID here 
  4.  $q[]                        = "trashed=false";                                  // display items not trashed 
  5.  $api['gdrive']['query']     = str_replace(' ', '+', implode(' and ', $q));      // join clauses with ' and ' and replace spaces with pluses 
  6.   
  7.  // send request to find a file in this folder (checking if file already exists) 
  8.  $url                        = 'https://www.googleapis.com/drive/v3/files?q='$api['gdrive']['query']
  9.  $header[]                   = 'Content-Type: application/x-www-form-urlencoded'
  10.  $header[]                   = 'Authorization: Bearer '.$access_token
  11.  $data                       = array()
  12.  $api['gdrive']['listing']   = send_request($url, $header, $data, "GET")

Additional Note(s)
The script will output all the variables it needs which includes private information such as the keys, visible to anyone running the script. So my full script will delete these variables from the $api output with the following lines:
copyraw
unset($api['keys']);
unset($api['jwt']);
unset($api['gapis']['oauth']);
  1.  unset($api['keys'])
  2.  unset($api['jwt'])
  3.  unset($api['gapis']['oauth'])
Displaying the rest of $api as a JSON string (in pretty print) is more for debugging purposes.

I could have replaced "Bearer" with whatever the access token type was but getting this working has been a little overdue already...

If you have any suggestions or queries, feel free to comment below and I'll try to respond in kind. Hopefully this article has been helpful for others out there.


Helpful Link(s):
Category: Google :: Article: 665

Credit where Credit is Due:


Feel free to copy, redistribute and share this information. All that we ask is that you attribute credit and possibly even a link back to this website as it really helps in our search engine rankings.

Disclaimer: The information on this website is provided without warranty and any content is merely the opinion of the author. Please try to test in development environments prior to adapting them to your production environments. The articles are written in good faith and, at the time of print, are working examples used in a commercial setting.

Thank you for visiting and, as always, we hope this website was of some use to you!

Kind Regards,

Joel Lipman
www.joellipman.com

Related Articles

Joes Revolver Map

Joes Word Cloud

Accreditation

Badge - Certified Zoho Creator Associate
Badge - Certified Zoho Creator Associate

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Paypal:
Donate to Joel Lipman via PayPal

Bitcoin:
Donate to Joel Lipman with Bitcoin - Valid till 8 May 2022 3QnhmaBX7LQSRsC9hh6Je9rGQKEGNQNfPb
© 2021 Joel Lipman .com. All Rights Reserved.