I am trying to sign an AWS API request and then use cURL.
The purpose is to submit a tracking number to API of the service provider, and use the response.
I am a complete noob to AWS API and cannot find my fault after numerous testing.
I have tried numerous ways but all leads to {"message":"Forbidden"}
.
Here is my current script:
<?php
$accessKeyId = "AKIA55D**********";
$secretAccessKey = "NQ0xcl**********";
$method ='GET';
$uri = '/tracking/shipments';
$secretKey = $secretAccessKey;
$access_key = $accessKeyId;
$region = 'af-south-1';
$service = 'execute-api';
$host = "https://api.shiplogic.com";
$alg = 'sha256';
$date = new DateTime('Africa/Johannesburg');
$dd = $date->format( 'Ymd\THis\Z' );
$amzdate2 = new DateTime( 'Africa/Johannesburg' );
$amzdate2 = $amzdate2->format( 'Ymd' );
$amzdate = $dd;
$algorithm = 'AWS4-HMAC-SHA256';
$canonical_uri = $uri;
$canonical_querystring = '';
$canonical_headers = "host:".$host."\n"."x-amz-date:".$amzdate."\n";
$signed_headers = 'host;x-amz-date';
$canonical_request = "".$method."\n".$canonical_uri."\n".$canonical_querystring."\n".$canonical_headers."\n".$signed_headers;
$credential_scope = $amzdate2 . '/' . $region . '/' . $service . '/' . 'aws4_request';
$string_to_sign = "".$algorithm."\n".$amzdate ."\n".$credential_scope."\n".hash('sha256', $canonical_request)."";
//string_to_sign is the answer..hash('sha256', $canonical_request)//
$kSecret = 'AWS4' . $secretKey;
$kDate = hash_hmac( $alg, $amzdate2, $kSecret, true );
$kRegion = hash_hmac( $alg, $region, $kDate, true );
$kService = hash_hmac( $alg, $service, $kRegion, true );
$kSigning = hash_hmac( $alg, 'aws4_request', $kService, true );
$signature = hash_hmac( $alg, $string_to_sign, $kSigning );
$authorization_header = $algorithm . ' ' . 'Credential=' . $access_key . '/' . $credential_scope . ', ' . 'SignedHeaders=' . $signed_headers . ', ' . 'Signature=' . $signature;
$headers = [
'content-type'=>'application/json',
'x-amz-date'=>$amzdate,
'Authorization'=>$authorization_header];
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.shiplogic.com/tracking/shipments?tracking_reference=M3RPH',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'X-Amz-Date: '.$amzdate.'',
'Authorization: ' . $authorization_header . ''
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
The response I receive is {"message":"Forbidden"}
What am I doing wrong?
CodePudding user response:
Install Amazon's PHP SDK as already pointed out in the comments.
composer require aws/aws-sdk-php
The documentation of the API you're trying to call has 1 PHP code sample and it uses that SDK. All you need to do is insert your credentials.
use Aws\Credentials\Credentials;
use Aws\Signature\SignatureV4;
use Psr\Http\Message\RequestInterface;
function sign(
RequestInterface $request,
string $accessKeyId,
string $secretAccessKey
): RequestInterface {
$signature = new SignatureV4('execute-api', 'af-south-1');
$credentials = new Credentials($accessKeyId, $secretAccessKey);
return $signature->signRequest($request, $credentials);
}
You can see both the input $request
and the return value are of the same RequestInterface
type. So it gives you a modified request object with a proper signature (if your key and params are valid).
If you're using curl
, you may not have this request object yet. Amazon's API uses the PSR-7 standard request interface, most frameworks implement this if they have a request object.
If you don't have a request object at hand, you could use Guzzle's implementation for it. The required packages are already installed as dependencies of Amazon's SDK. You can install both highlighted packages from the Github permalink.
$request = new GuzzleHttp\Psr7\Request(
'GET',
'https://api.shiplogic.com/tracking/shipments',
[
// Maybe you still need headers.
]
);
$signed_request = sign($request);
$client = new \GuzzleHttp\Client();
$response = $client->send($signed_request);
As mentioned in the comments creating your own implementation of security components should be avoided as much as possible. It's a lot easier to rewrite your code to use this request object, especially for future maintenance.
Also keep in mind that your credentials might simply be invalid, or have no access to the resource you're requesting. At first sight most of your implementation seemed to follow what's expected from the signing protocol (though you just need 1 tiny mistake and it won't work), so if you're sure you checked everything it might be as simple as that.