Google API OAuth 2.0 authentication returning "invalid_grant" - possible issue with private key signing signature?

I am trying to connect to a Google API that requires OAuth 2.0 authentication. In the first step, I need to create a JSON Web Token (JWT): https://developers.google.com/accounts/docs/OAuth2ServiceAccount?hl=en#creatingjwt in the following format:

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

      

I follow all instructions as per the documentation, but keep getting "invalid_grant" error. My suspicion is that this is related to the last (signature) part. According to the documentation:

"The signature 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 the 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 SHA-256 hash function) with the private key obtained from the google developer console. Byte array."

"The signature must be Base64url encoded."

I copied the private key directly from the Google Dev console (including the "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 an "invalid_grant" error?

+3


source to share


2 answers


You have 2 problems:

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

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

      

Replace calls base64_encode

base64url_encode

Second: You never sign your data! You just add your private key, not the header and requirement set signature. 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]');

      

from

$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


source


You are getting invalid_grant because you did

1.

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

      

change it to

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

      

2.

iss the claim field is NOT the ClientID from the credentials. This email address is IS.

3.

The time must be in UTC, so you must do



$time = gmmktime();

      

4.

As already mentioned @ florent-morselli you need to sign the signature. It's right.

five.

Check the operating time of the device. It is important that the time is synchronized.

After that, everything will be fine and Google will give you 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

      

0


source







All Articles