3
votes

I am attempting to connect to Google API which requires OAuth 2.0 authentication. The first step requires me to generate a JSON Web Token (JWT): https://developers.google.com/accounts/docs/OAuth2ServiceAccount?hl=ru#creatingjwt with the following format:

{Base64url encoded header}.
{Base64url encoded claim set}.
{Base64url encoded signature}

I'm following all of the directions as per the documentation, but continue to get an "invalid_grant" error. My suspicion is that this is related to the last (signature) portion. According to the documentation:

"The signing algorithm in the JWT header must be used when computing the signature. The only signing algorithm supported by the Google OAuth 2.0 Authorization Server is RSA using SHA-256 hashing algorithm. This is expressed as RS256 in the alg field in the JWT header."

"Sign the UTF-8 representation of the input using SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function) with the private key obtained from the Google Developers Console. The output will be a byte array."

"The signature must then be Base64url encoded."

I copied the private key directly from the Google Dev Console (including "BEGIN PRIVATE KEY" part) and base64 encoded it. This is the code I am using:

$time = time();
$url = 'https://accounts.google.com/o/oauth2/token';
$assertion = base64_encode('{"alg":"RS256","typ":"JWT"}').'.';
$assertion .= base64_encode('{
   "iss":"'.$this->configs['client_id'].'",
   "scope":"https://www.googleapis.com/auth/youtube",   
   "aud":"'.$url.'",
   "exp":'.($time+3600).',
   "iat":'.$time.'
  }').'.';
$assertion .= base64_encode('[-----BEGIN PRIVATE KEY-----privateKeyHere-----END PRIVATE KEY-----\n]');
$headers = array(
      'Host: accounts.google.com',
      'Content-Type: application/x-www-form-urlencoded'
);
$fields = array(
   'grant_type' => urlencode('urn:ietf:params:oauth:grant-type:jwt-bearer'),
   'assertion'  => urlencode($assertion)
);

$fields_string = '';
foreach($fields as $key => $value) $fields_string .= '&'.$key.'='.$value;
$fields_string = substr($fields_string, 1);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url );
curl_setopt($ch, CURLOPT_HEADER, TRUE );
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers );
curl_setopt($ch, CURLOPT_POST, count($fields));
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1 );
$result = curl_exec($ch);
curl_close($ch);
var_dump($result);

Any ideas as to why this is returning the "invalid_grant" error?

2

2 Answers

1
votes

You have 2 problems:

First one: the PHP function base64_encode is not URL safe as required by the JWT specification. To get a Base64Url string, use the following function:

function base64url_encode($data)
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

Replace your base64_encode calls with base64url_encode

Second one: You never sign your data! You just add your private key, not a signature of the header and claim set. Use the following function:

function sign($input, $private_key)
{
    $signature = null;
    openssl_sign($input, $signature, $private_key, "SHA256");
    return $signature;
}

And replace

$assertion .= base64_encode('{
  "iss":"'.$this->configs['client_id'].'",
  "scope":"https://www.googleapis.com/auth/youtube",   
  "aud":"'.$url.'",
  "exp":'.($time+3600).',
  "iat":'.$time.'
}').'.';
$assertion .= base64_encode('[-----BEGIN PRIVATE KEY-----privateKeyHere-----END PRIVATE KEY-----\n]');

with

$assertion .= base64_encode('{
  "iss":"'.$this->configs['client_id'].'",
  "scope":"https://www.googleapis.com/auth/youtube",   
  "aud":"'.$url.'",
  "exp":'.($time+3600).',
  "iat":'.$time.'
}');
$assertion .= '.'.base64url_encode(sign($assertion, '[-----BEGIN PRIVATE KEY-----privateKeyHere-----END PRIVATE KEY-----\n]'));
0
votes

You receive invalid_grant because you did

1.

'grant_type' => urlencode('urn:ietf:params:oauth:grant-type:jwt-bearer')

change it to just

'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer'

2.

iss field of assertion IS NOT A ClientID from credentials. This IS Email address.

3.

Time should be in UTC, so you should do

$time = gmmktime();

4.

As mentioned @florent-morselli you need to sign the signature. This is true.

5.

Check time of your machine. It's important that time should be synchronized

After this everything will be ok and Google give you an access_token.

BTW:

  privateKey should be without []\n characters (In my case this is lines of 64 symbols)

  CURLOPT_POST takes only true or false

  http_build_query is better way to build queryString

  'Host: accounts.google.com' in $headers is useless, because you set CURLOPT_URL