Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Signing URLs with JWT for Google Cloud Storage using PHP

I've just started to upgrade my Google Cloud Storage code from API version 1.0 to version 2.0 and I'm having some troubles.

With version 1.0 I used Signed URLs with great success, using .p12 files. However that's deprecated in the new version and I have to use Firebase/php-jwt instead, using JSON files.

The problem is that it's just not working, I get the error:

<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message>
<StringToSign>PUT

image/png
1483626991
/myBucket/folder/test.PNG</StringToSign></Error>

This is the simplified code used to sign it.

$string = ($method . "\n" .
          $contentMd5 . "\n" .
          $contentType . "\n" .
          $expiration . "\n" .
          $file);

$signedURL = base64_encode(Firebase\JWT\JWT::encode($string,
        file_get_contents($credentialsFilePath)));

After the signedURL is received I build an URL with the correct data. The only part I've changed from 1.0 and 2.0 is the part where you sign the URL. Furthermore I've checked that the string in "StringToSign"-field of the response is exactly the same as the one I'm signing.

In version 1.0 I signed the URL like this:

$signedURL = base64_encode((new Google_Signer_P12(
        file_get_contents($p12FilePath),
        'notasecret'
      ))->sign($string));

All of this leads me to believe that I'm singing the correct contents but using the JWT function the wrong way. Has anyone else done this? How did yo do it?

In case it's interesting this is the URL I build (works with 1.0):

$returnArr['url'] = "https://{$bucket}.commondatastorage.googleapis.com/"
    . $prefix . '/' . rawurlencode($file)
    . "?GoogleAccessId=" . rawurlencode($serviceEmail)
    . "&Expires={$expiration}"
    . "&Signature=" . rawurlencode($signature);
like image 686
JohanLejdung Avatar asked Jan 05 '17 15:01

JohanLejdung


People also ask

How do I authenticate Google Cloud Storage?

API authenticationIn the OAuth 2.0 Playground, click Cloud Storage API v1, and then select an access level for your application ( full_control , read_only , or read_write ). Click Authorize APIs. Sign in to your Google account when prompted. In the dialogue that appears, click Allow.

Can I upload files to Google Cloud Storage from URL?

Cloud Storage provides the Signed URL feature to let individual end users perform specific actions. Signed URL makes it possible to generate temporary credentials valid only for a specific end user to securely upload a file. The Google Cloud official client library makes it easy to generate a Signed URL.

What is signed URL in GCS?

Overview. A signed URL is a URL that provides limited permission and time to make a request. Signed URLs contain authentication information in their query string, allowing users without credentials to perform specific actions on a resource.


1 Answers

Looking at the source for that JWT library the first thing that jumps out at me, and I see was noted in comments, is that your payload should be an array or object, not a string... "JSON web tokens".

* @param object|array  $payload    PHP object or array

public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)

Second, it looks like you are double base64 encoding it... base128? :) The return value of encode should be the three Base64url strings concatenated together, so you shouldn't need to do it again.

I'd give this a try:

$payload = ['HTTP_Verb'              => $method,
            'Content_MD5'            => $contentMd5,
            'Content_Type'           => $contentType,
            'Expiration'             => $expiration,
            'Canonicalized_Resource' => $file];

$key = file_get_contents($credentialsFilePath);
$signedURL = Firebase\JWT\JWT::encode($payload, $key); //think base64_encode here is redundant.

Ref: Overview of Signed URLs page. They sure don't explain things very well in those docs. I assume you've looked at SDK?

If you wanted to go the string route you would need to sign using RSA signatures with SHA256... opensssl_sign or also maybe easier to lean on Google's PHP SDKs?

Later...

OK, decided to test it. Saw Google Cloud had a free trial. Installed gsutil, read a bunch of docs. Damned if I understand this JWT approach though. Share if anyone can even provide the docs on that topic.

This code works:

<?php
$method = 'GET';
$expires = '1503532674';
$container = '/example-bucket/cat.jpeg';

$payload = "{$method}\n\n\n{$expires}\n{$container}";

//assume you have this 'json' formatted key too? Otherwise just load the private key file as is.
$key = file_get_contents('~/oas_private_key.json');
$key = json_decode($key, true);
$key = $key['private_key'];

//if sucessful the encypted string is assigned to $signature
openssl_sign($payload, $signature, $key, OPENSSL_ALGO_SHA256);

$signature = urlencode(base64_encode($signature));    

die("https://storage.googleapis.com/{$container}[email protected]&Expires={$expires}&Signature={$signature}");    

Finally no "SignatureDoesNotMatch" error! Personally I'd use the SDK. Little bit of init and you can just do something like the following:

$url = $object->signedUrl(new Timestamp(new DateTime('tomorrow')), [
    'method' => 'PUT'
]);

It would also make upgrades easier in the future.

like image 131
ficuscr Avatar answered Oct 20 '22 10:10

ficuscr