Im able to put a file (image.png) on to my Google Cloud Storage bucket using the google-api-php-client, but now im having trouble trying to create a signed url to get access to the file from my website. Sample code:
$bucketName = 'bucket-name';
$id = 'image.png';
$serviceAccountName = '123456789-xxxx#developer.gserviceaccount.com';
$privateKey = file_get_contents($location_to_key_file);
$signer = new \Google_P12Signer($privateKey, "notasecret");
$ttl = time() + 3600;
$stringToSign = "GET\n" . "\n" . "\n" . $ttl . "\n". '/' . $bucketName . '/' . $id;
$signature = $signer->sign(utf8_encode($stringToSign));
$finalSignature = \Google_Utils::urlSafeB64Encode($signature);
$host = "https://".$bucketName.".storage.googleapis.com";
echo $host. "/".$id."?GoogleAccessId=" . $serviceAccountName . "&Expires=" . $ttl . "&Signature=" . $finalSignature;
Returns:
<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>
GET 1387590477 /bucketname/image.png</StringToSign></Error>
im using google-api-php-client with php 5.5
ive followed a few examples:
https://groups.google.com/forum/#!topic/gs-discussion/EjPRAWbWKbw
https://groups.google.com/forum/#!msg/google-api-php-client/jaRYDWdpteQ/xbNTLfDhUggJ
Maybe a config value im not passing correctly ?
i assume the Service Account email should be used. Also tried to include md5hash and content-type in the $stringToSign, same results.
any help/tips would be appreciated.
The rdb almost do the trick for me. I used a working python example from GoogleCloudPlatform for python in order to debug what was wrong with the url and find the following:
The GoogleAccessId have to be urlencoded
You've to replace in the Signature the following characters: '-' => '%2B', '_' => '%2F
The signature must end with '%3D'
Code:
$host. "/".$id."?Expires=" . $ttl . "&GoogleAccessId=" .
urlencode($serviceAccountName) . "&Signature=" .
str_replace(array('-','_',), array('%2B', '%2F'),urlencode($finalSignature)).'%3D';
Now the url should work and you can use some advanced operators like response-content-disposition or response-content-type
Can you try constructing signed URL by using $host as -
$host = "https://".$bucketName.".commondatastorage.googleapis.com";
There is one difference I found with the doc you are referring.
Thanks
can you try this codes :)
$finalSignature = base64_encode($signature);
echo $host. "/".$id."?GoogleAccessId=" . $serviceAccountName . "&Expires=" . $ttl . "&Signature=" . urlencode($finalSignature);
I think your error is in the $finalSignature = \Google_Utils::urlSafeB64Encode($signature); line. This method does something weird with the URL and replaces certain characters.
In the end I got it all working with the following code:
$expires = time() + 60 * 30; // Half an hour
// Get the key from the key file
$privateKeyPath = Config::get('gcs.signing.key');
$privateKey = file_get_contents($privateKeyPath);
$signer = new Google_Signer_P12($privateKey, Config::get('gcs.signing.password'));
//Signing does not like spaces, however it also doesn't like urlencoding or html entities
$cloudStoragePath = str_replace(' ', '%20', $cloudStoragePath);
//Create string to sign
$stringToSign = "GET\n\n\n" . $expires . "\n" . "/" . $cloudStoragePath;
//Sign
$signature = $signer->sign(utf8_encode($stringToSign));
$query = array(
'GoogleAccessId' => Config::get('gcs.signing.service_account'),
'Expires' => $expires,
'Signature' => base64_encode($signature)
);
$url = self::$storageBaseUrl . '/' . $cloudStoragePath . '?' . http_build_query($query);
Related
I've been stuck on this problem for like two days. I've written a Python script which makes a PUT request to AWS Pinpoint service.
Pinpoint like many other AWS services requires a signature authentification on requests, which I managed to handle in Python.
Right now I'm trying to translate my script into a PHP service for Symfony. When I run my first request to AWS pinpoint I get this:
The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
The Canonical String for this request should have been\n'PUT\n/v1/apps/.../endpoints/...\n\ncontent-type:application/json\nhost:pinpoint.eu-west-1.amazonaws.com\nx-amz-content-sha256:de98d86577f0e1...655e6de27154af1c05ab34\nx-amz-date:20191226T151542Z\nx-amz-security-token:IQoJb....\nx-amz-user-agent:aws-amplify/1.1.2 react-native aws-amplify/1.1.2 react-native callback\n\ncontent-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent\n0240a9479d0a66d74eaae42dc...95247aaa800fcbe5cf2
The String-to-Sign should have been
'AWS4-HMAC-SHA256\n20191226T151542Z\n20191226/eu-west-1/mobiletargeting/aws4_request\nb2c451534fe370503ecf4068b45c...63e91280cc3187ae3230034107
So I already checked if my Canonical String was wrong, it is the exact same AWS is asking. The String-to-Sign is different by the Canonical String hash.
Here's my headers function
public function create_headers($data,\DateTime $time,$canonical_uri,$method,$to_api=null)
{
$amz_date = $time->format('Ymd\THis\Z');
$date_stamp = $time->format('Ymd');
$payload_hash = hash('sha256',$data);#utf8_encode($data));
$canonical_querystring = "";
$canonical_headers = 'content-type:' . $this->content_type . '\n' . 'host:' . $this->host . '\n' . 'x-amz-content-sha256:' . $payload_hash . '\n' . 'x-amz-date:' . $amz_date . '\n' . 'x-amz-security-token:' . $this->security_token . '\n' . 'x-amz-user-agent:aws-amplify/1.1.2 react-native aws-amplify/1.1.2 react-native callback' . '\n';
$signed_headers = 'content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent';
$canonical_request = $method . '\n' . $canonical_uri . '\n' . $canonical_querystring . '\n' . $canonical_headers . '\n' . $signed_headers . '\n' . $payload_hash;
echo '<br><br>';
print_r(str_replace('\n','<br>',$canonical_request));
#var_dump($canonical_request);
$algorithm = 'AWS4-HMAC-SHA256';
$credential_scope = "{$date_stamp}/{$this->region}/{$this->service}/aws4_request";
#$date_stamp . '/' . $this->region . '/' . $this->service . '/' . 'aws4_request';
#$credential_scope = $this->createScope($date_stamp,$this->region,$this->service);
echo '<br><br>';
#$string_to_sign = $algorithm . '\n' . $amz_date . '\n' . $credential_scope . '\n' . hash('sha256', utf8_encode($canonical_request));
$hash = hash('sha256', $canonical_request);
$string_to_sign = "AWS4-HMAC-SHA256\n{$amz_date}\n{$credential_scope}\n{$hash}";
print_r(str_replace('\n','<br>',$string_to_sign));
echo '<br><br>';
$signing_key = $this->get_signature_key($this->secret_key,$date_stamp,$this->region,$this->service);
$signature = hash_hmac('sha256',$string_to_sign,$signing_key);
$authorization_header = $algorithm . ' ' . 'Credential=' . $this->access_key . '/' . $credential_scope . ', ' . 'SignedHeaders=' . $signed_headers . ', ' . 'Signature=' . $signature;
$headers = array(
'host'=> $this->host,
'content-type'=> $this->content_type,
'x-amz-user-agent'=> 'aws-amplify/1.1.2 react-native aws-amplify/1.1.2 react-native callback',
'x-amz-content-sha256'=> $payload_hash,
'x-amz-security-token'=> $this->security_token,
'x-amz-date'=> $amz_date,
'authorization'=> $authorization_header
);
$this->s->headers = $headers;
return $headers;
}
I've been looking for my error for days but I think I need someone with a fresh eye...
Thank you!
After hours and hours of questionning myself, I've finally found why I was getting that error.
In PHP "\n" and '\n' doesn't have the same meaning.
"\n" is a real line break - which is what AWS is asking.
'\n' is a string of \ and n characters.
AWS API is still pretty shit though.
I'm using this class to generate a direct upload form which includes the policy part.
https://designedbyaturtle.com/direct-upload-to-s3-using-aws-signature-v4-php/
The uploads are working but I want to be able to display the file from the url for users on the site without making the files public.
I understand the SDK has a simple method for this but I am hoping I can do it with the existing code as this already creates the policy. I'm wondering what are hte steps for creating this url from scratch? It seems excessive to include the entire bloated SDK for just one function.
I solved it by using these two classes and modifying them a bit to make a class with two functions geturl and getform instead of using the API, these work great for v4 signatures.
getform:
https://www.designedbyaturtle.co.uk/2015/direct-upload-to-s3-using-aws-signature-v4-php/
geturl:
https://gist.github.com/anthonyeden/4448695ad531016ec12bcdacc9d91cb8
Creating an S3 pre-signed URL is actually very easy for GET requests. PUT is fairly easy, but POST is complicated and requires a policy.
The challenge is creating the signing code. Amazon supports two versions v2 and v4. v2 is being phased out. v4 is somewhat complicated to code.
If you are only creating pre-signed URLs for GET requests, write your own code. For anything else, I seriously recommend using the SDK.
Below is a link to the source code to pre-sign an S3 URL using S3V4 using PHP without the AWS SDK.
S3LINK-V4.PHP
I wrote a function in php using #xmxmxmx answer and it is working fine with me
function AWS_S3_PresignDownload($AWSAccessKeyId, $AWSSecretAccessKey, $BucketName, $AWSRegion, $canonical_uri, $expires = 8400)
{
$encoded_uri = str_replace('%2F', '/', rawurlencode($canonical_uri));
// Specify the hostname for the S3 endpoint
if ($AWSRegion == 'us-east-1') {
$hostname = trim($BucketName . ".s3.amazonaws.com");
$header_string = "host:" . $hostname . "\n";
$signed_headers_string = "host";
} else {
$hostname = trim($BucketName . ".s3-" . $AWSRegion . ".amazonaws.com");
$header_string = "host:" . $hostname . "\n";
$signed_headers_string = "host";
}
$currentTime = time();
$date_text = gmdate('Ymd', $currentTime);
$time_text = $date_text . 'T' . gmdate('His', $currentTime) . 'Z';
$algorithm = 'AWS4-HMAC-SHA256';
$scope = $date_text . "/" . $AWSRegion . "/s3/aws4_request";
$x_amz_params = array(
'X-Amz-Algorithm' => $algorithm,
'X-Amz-Credential' => $AWSAccessKeyId . '/' . $scope,
'X-Amz-Date' => $time_text,
'X-Amz-SignedHeaders' => $signed_headers_string
);
// 'Expires' is the number of seconds until the request becomes invalid
$x_amz_params['X-Amz-Expires'] = $expires + 30; // 30seocnds are less
ksort($x_amz_params);
$query_string = "";
foreach ($x_amz_params as $key => $value) {
$query_string .= rawurlencode($key) . '=' . rawurlencode($value) . "&";
}
$query_string = substr($query_string, 0, -1);
$canonical_request = "GET\n" . $encoded_uri . "\n" . $query_string . "\n" . $header_string . "\n" . $signed_headers_string . "\nUNSIGNED-PAYLOAD";
$string_to_sign = $algorithm . "\n" . $time_text . "\n" . $scope . "\n" . hash('sha256', $canonical_request, false);
$signing_key = hash_hmac('sha256', 'aws4_request', hash_hmac('sha256', 's3', hash_hmac('sha256', $AWSRegion, hash_hmac('sha256', $date_text, 'AWS4' . $AWSSecretAccessKey, true), true), true), true);
$signature = hash_hmac('sha256', $string_to_sign, $signing_key);
return 'https://' . $hostname . $encoded_uri . '?' . $query_string . '&X-Amz-Signature=' . $signature;
}
Call it using
echo AWS_S3_PresignDownload('accessId', 'seceret', 's3BucketName', 'reGion', '/fileKey.ext', 60);
The method i tried using was with openssl
$fp = fopen($key, 'r'); //open the PEM file
$priv_key = fread($fp,8192);
fclose($fp);
$pkeyid = openssl_get_privatekey($priv_key,"password");
openssl_sign($response["data_to_sign"], $signature, $pkeyid,'sha256');
$sign = base64_encode($signature)
Is this the correct Method to generate signature for signed urls in google?
You can try Google Cloud Storage PHP SDK, it's a good choice for keeping your codes clean.
cloud-storage PHP SDK
Install package to your project by following this page
on Packagist,
then
function getSignedGcsUrl($objPath/* which is your target object path */, $duration = 50)
{
$storageClient = new StorageClient([
'projectId' => /* your gcp projectId here */,
'keyFilePath' => /* your gcp keyFilePath here */,
]);
$bucket = $storageClient->bucket($objPath);
$object = $bucket->object();
$url = $object->signedUrl(new \DateTime('+ ' . $duration . ' seconds'));
return $url;
}
laravel-google-cloud-storage (for Laravel)
Install and configurate superbalist/laravel-google-cloud-storage by following this page:
on Github,
then
public static function getSignedGcsUrl($objPath, $duration = 50)
{
return Storage::disk('gcs'/* following your filesystem configuration */)
->getAdapter()
->getBucket()
->object($objPath)
->signedUrl(new \DateTime('+ ' . $duration . ' seconds'));
}
I put all the answers together. This should work in out of the box project. If you have space in the paths, you will need to rawurlencode the individual components, not urlencode.
function signedGoogleStorageURL($bucketName, $resourcePath, $duration = 10, $method = 'GET')
{
$expires = time() + $duration;
$content_type = ($method == 'PUT') ? 'application/x-www-form-
urlencoded' : '';
$to_sign = ($method . "\n" .
/* Content-MD5 */ "\n" .
$content_type . "\n" .
$expires . "\n" .
"/" . $bucketName . $resourcePath);
$sign_result = AppIdentityService::signForApp($to_sign);
$signature = urlencode(base64_encode($sign_result['signature']));
$email = AppIdentityService::getServiceAccountName();
return ('https://storage.googleapis.com/' . $bucketName .
$resourcePath .
'?GoogleAccessId=' . $email .
'&Expires=' . $expires .
'&Signature=' . $signature);
}
$signedPath = signedGoogleStorageURL(AppIdentityService::getDefaultVersionHostname(), "/my_folder/my_file", 60);
One thing to note that I spent about two hours on:
The GoogleAccessId you pass into the URL is the Email Address in the "Certificate" section of the Google Cloud Console. It's not the OAuth Client ID with a string replacement as Google suggests in their documentation.
There's an example here that signs a URL for Google Cloud Storage using PHP:
https://groups.google.com/forum/#!msg/google-api-php-client/jaRYDWdpteQ/xbNTLfDhUggJ
However - I note this is tagged with Google App Engine... If your code is running inside of Google App Engine, you should use the built-in App Identity service - (note this will only work once your application is deployed in production, not while running locally) - this means you will not need to download or handle any private keys:
require_once 'google/appengine/api/app_identity/AppIdentityService.php';
$sign_result = AppIdentityService::signForApp( $message );
You will need to make sure that the service account associated with the App Engine application is added to the team for the project that owns the Cloud Storage bucket.
I am using Cloud File API of Rackspace Cloud server in PHP, I want to generate a temp url to download files for direct to my server for this i am using get_temp() method of this api but before use to this method i have to set Meta Data key for my container. How would i do this.
public function get_temp_url($key, $expires, $method)
{
$expires += time();
$url = $this->container->cfs_http->getStorageUrl() . '/' . $this->container->name . '/' . $this->name;
return $url . '?temp_url_sig=' . hash_hmac('sha1', strtoupper($method) .
"\n" . $expires . "\n" . parse_url($url, PHP_URL_PATH), $key) .
'&temp_url_expires=' . $expires;
}
The comments on this page include an example of how to set this:
http://docs.rackspace.com/files/api/v1/cf-devguide/content/Set_Account_Metadata-d1a4460.html
Also, if you use the new Cloud Files API...
https://github.com/rackspace/php-opencloud
...it includes a SetTempUrlSecret method in the ObjectStore class that will do this for you.
I am able to get access_token for multiple permissions like emails, contacts, docs, etc. using oAuth 2.0. I have access_token
I got contacts using the following code.
$url = 'https://www.google.com/m8/feeds/contacts/default/full?max- results='.$max_results.'&oauth_token='.$access_token;
$response_contacts= curl_get_file_contents($url);
Now i want to get users Emails using this access_token.
i used this url . but it gives 401 unauthorized Error
$url = 'https://mail.google.com/mail/feed/atom&oauth_token='.$access_token;
$response_emails= curl_get_file_contents($url);
please guide me how can i get emails using access_token.
I've seen references to the Gmail feed using oauth_token as a request parameter. However, once I used the OAuth Playground I discovered that you need to pass your OAuth information as an Authorization header, as you'll see below.
<?php
$now = time();
$consumer = ...; // your own value here
$secret = ...; // your own value here
$nonce = ...; // same value you've been using
$algo = "sha1";
$sigmeth = "HMAC-SHA1";
$av = "1.0";
$scope = "https://mail.google.com/mail/feed/atom";
$path = $scope;
$auth = ...; // an object containing outputs of OAuthGetAccessToken
$args = "oauth_consumer_key=" . urlencode($consumer) .
"&oauth_nonce=" . urlencode($nonce) .
"&oauth_signature_method=" . urlencode($sigmeth) .
"&oauth_timestamp=" . urlencode($now) .
"&oauth_token=" . urlencode($auth->oauth_token) .
"&oauth_version=" . urlencode($av);
$base = "GET&" . urlencode($path) . "&" . urlencode($args);
$sig = base64_encode(hash_hmac($algo, $base,
"{$secret}&{$auth->oauth_token_secret}", true));
$url = $path . "?oauth_signature=" . urlencode($sig) . "&" . $args;
// Create a stream
$opts = array(
"http" => array(
"method" => "GET",
"header" => "Authorization: OAuth " .
"oauth_version=\"{$av}\", " .
"oauth_nonce=\"{$nonce}\", " .
"oauth_timestamp=\"{$now}\", " .
"oauth_consumer_key=\"{$consumer}\", " .
"oauth_token=\"{$auth->oauth_token}\", " .
"oauth_signature_method=\"{$sigmeth}\", " .
"oauth_signature=\"{$sig}\"\r\n"
)
);
$context = stream_context_create($opts);
$out = file_get_contents($path, false, $context);
?>