How do I sign AWS API requests with PHP? - php

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?

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.

Related

Invalid login attempt. Netsuite Token-based Authentication (TBA) with REST Web Services returns InvalidSignature for query with params

When I query NS API using this URL:
https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql
It returns to me whatever I specify in my body (e.g. SELECT * FROM customer)
However, when I try to query it using this URL(it has a limit param):
https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=6
It does not work and returns the error showed down below
{
"type":"https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2",
"title":"Unauthorized",
"status":401,
"o:errorDetails":[
{
"detail":"Invalid login attempt. For more details, see the Login Audit Trail in the NetSuite UI at Setup > Users/Roles > User Management > View Login Audit Trail.",
"o:errorCode":"INVALID_LOGIN"
}
]
}
I suppose the problem could be either in the way I pass parameters (e.g. limit) or in the generation of the signature.
If I run this query in PostMan it works, but when it comes to PHP it does not work with any parameters I pass to Netsuite API.
If I open "Login Audit Trail Search", you can see that URL column has no ?limit=7 just like this: /services/rest/query/v1/suiteql, and the "Detail" columns says InvalidSignature
The code I am using within PHP is:
$httpMethod ="POST";
$accountID = 'NNNNNNN-sb1';
$realm = "NNNNNNN_SB1";
// this one is working fine
$url = 'https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql';
// when adding this limit it throws the error in PHP code (but postman works fine)
$url = 'https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=6';
$ckey = "NNNNN"; //Consumer Key
$csecret = "NNNNN"; //Consumer Secret
$tkey = "NNNNN"; //Token ID
$tsecret = "NNNNN"; //Token Secret
$timestamp = time();
$nonce = md5(mt_rand());
$signatureMethod = 'HMAC-SHA256';
$data["q"] = "SELECT * FROM customer WHERE LENGTH(externalid) = 32";
$baseString = $httpMethod . '&' . rawurlencode($url) . '&'
. rawurlencode(
"oauth_consumer_key=" . rawurlencode($ckey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($signatureMethod)
. "&oauth_timestamp=" . rawurlencode($timestamp)
. "&oauth_token=" . rawurlencode($tkey)
. "&oauth_version=1.0"
);
$key = rawurlencode($csecret) . '&' . rawurlencode($tsecret);
$signature = base64_encode(hash_hmac('sha256', $baseString, $key, true));
$signature = rawurlencode($signature);
$header = array(
"Authorization: OAuth realm=\"$realm\", oauth_consumer_key=\"$ckey\", oauth_token=\"$tkey\", oauth_nonce=\"$nonce\", oauth_timestamp=\"$timestamp\", oauth_signature_method=\"$signatureMethod\", oauth_version=\"1.0\", oauth_signature=\"$signature\"",
'Cookie: NS_ROUTING_VERSION=LAGGING',
'prefer: transient',
'Content-Type: text/plain',
'Accept: */*',
'Content-length: ' . strlen(json_encode($data)),
);
$curl = curl_init();
$opts = array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $httpMethod,
CURLOPT_HTTPHEADER => $header,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => json_encode($data)
);
curl_setopt_array($curl, $opts);
$response = curl_exec($curl);
curl_close($curl);
Screenshots (netsuite settings and postman):
https://imgur.com/a/LOy0k8T
NS - netsuite
A friend of mine helped me really much, and we have an answer to this ambiguous question.
Here is the code solution.
in the base string you must only have 2 ampersands w/o encoding and no encoding on baseUrl.
all params must be in alphabetical order
<?php
$httpMethod ="POST";
$accountID = 'NNNNNNN-sb1';
$realm = "NNNNNNN_SB1";
// this one is working fine
$baseUrl = 'https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql';
$ckey = "NNNNN"; //Consumer Key
$csecret = "NNNNN"; //Consumer Secret
$tkey = "NNNNN"; //Token ID
$tsecret = "NNNNN"; //Token Secret
$timestamp = time();
$nonce = md5(mt_rand());
$signatureMethod = 'HMAC-SHA256';
$data["q"] = "SELECT * FROM customer WHERE LENGTH(externalid) = 32";
$baseString = $httpMethod . '&' . rawurlencode($baseUrl) . '&'
. rawurlencode(
//APPLY PARAMETERS IN ALPHABETICAL ORDER, URL ENCODED IN HERE
"limit=2"
. "&oauth_consumer_key=" . rawurlencode($ckey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($signatureMethod)
. "&oauth_timestamp=" . rawurlencode($timestamp)
. "&oauth_token=" . rawurlencode($tkey)
. "&oauth_version=1.0"
. "&offset=2"
);
$key = rawurlencode($csecret) . '&' . rawurlencode($tsecret);
$signature = base64_encode(hash_hmac('sha256', $baseString, $key, true));
$signature = rawurlencode($signature);
$header = array(
"Authorization: OAuth realm=\"$realm\", oauth_consumer_key=\"$ckey\", oauth_token=\"$tkey\", oauth_nonce=\"$nonce\", oauth_timestamp=\"$timestamp\", oauth_signature_method=\"$signatureMethod\", oauth_version=\"1.0\", oauth_signature=\"$signature\"",
'Cookie: NS_ROUTING_VERSION=LAGGING',
'prefer: transient',
'Content-Type: text/plain',
'Content-length: ' . strlen(json_encode($data)),
);
$curl = curl_init();
$opts = array(
CURLOPT_URL => $baseUrl . '?limit=2&offset=2', // APPLY LIMIT MANUALLY HERE
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $httpMethod,
CURLOPT_HTTPHEADER => $header,
CURLOPT_POSTFIELDS => json_encode($data)
);
curl_setopt_array($curl, $opts);
$response = curl_exec($curl);
curl_close($curl);
var_dump($response);
exit;

Etsy API pagination - Don't know how to access to other pages

So, I had issue because I don't have OAuth 1, so to access to ETSY API I had to struggle.
finally I have this :
My function __construct() :
parent::__construct();
$this->allowForUser();
$this->etsyDb = new EtsyordersModel;
$this->client = new Etsy([
'identifier' => getApp()->getConfig('identifier'),
'secret' => getApp()->getConfig('secret'),
'scope' => 'listings_r transactions_r',
'callback_uri' => 'http://*****-*****.loc/etsyapp/next',
'access_token' => getApp()->getConfig('access_token'),
'access_token_secret' => getApp()->getConfig('access_token_secret'),
]);
And here is how I get the url to access my JSON with the result :
$key = rawurlencode($oauthtoken) . "&" . rawurlencode($tokensecret);
$method = "GET";
$baseurl = "https://openapi.etsy.com/v2/shops/24749672/receipts/";
$nonce = "1234";
$timenow = time();
$oauthversion = '1.0';
$paramstring = "oauth_consumer_key=" . $oauthconsumerkey . "&oauth_nonce=" . $nonce . "&oauth_signature_method=HMAC-SHA1" . "&oauth_timestamp=" . $timenow . "&oauth_token=" . $clientsecret . "&oauth_version=" . $oauthversion;
//
// Signature GET to retrieve signature OAuth
$encodeurl = $method . "&" . rawurlencode($baseurl) . "&" . rawurlencode($paramstring);
$signature = hash_hmac( 'sha1', $encodeurl, $key, TRUE );
$signature = base64_encode( $signature );
// url JSON shop ETSY
$curlurl = $baseurl . "?" . $paramstring . "&oauth_signature=" . $signature;
// get JSON content in $orders
$orders = file_get_contents($curlurl);
And so I get my JSON :
I got 90++ items, but because of the pagination, it show me only 25 items.
I tried to add active?limit=100&offset=0 to my $baseurl (at the end of it, as parameter for signature calculation) but it say oauth_problem=signature_invalid&. I tried to put it on the $paramstring
instead, but same result.
How can I have all my result shown, or how can I access to page 2, 3, etc... of the pagination?
I am on PHP 7.4, XAMPP (one of the last version), I don't have OAuth 1 so I used a combination of package Y0lk\OAuth1 and thephpleague\oauth1-client.
Thank you for your time.

How to generate signature in Shopee 2.0 API request authentication?

I'm trying to get shop details thru https://partner.uat.shopeemobile.com/api/v2/shop/get
I tried this in php
$timestamp = time();
$partner_key = 123456;
$path = '/api/v2/shop/get';
$access_token = 'token...';
$shop_id = 22222;
$base_string = $partner_key . $path . $timestamp . $access_token . $shop_id;
$secret_key = 'secret...';
$sign = hash_hmac('sha256', $base_string, $secret_key);
but the response returned is wrong_sign.
I'm not sure if the path is correct. But that is what I understood in their documentation. https://open.shopee.com/documents?module=63&type=2&id=56
$date = new DateTime();
$timestamp = $date->getTimestamp();
$partner_id = "xxxxxx";
$shop_id = "xxxxxx";
$secret_key = "yyyyyyyy";
$path = "/api/v2/auth/token/get"; //without the host
$base_str = $partner_id . $path . $timestamp . $shop_id;
$sign = hash_hmac('sha256', $base_str, $secret_key, false);
Shopee signatures depends on the API you are calling
for Shop: Signature generated by partner_id, api path, timestamp, access_token, shop_id and partner_key via HMAC-SHA256 hashing algorithm.
setup the base string by concatinating the partner_id, api_path, timestamp(UNIX format), access_token and shop_id, then hash the base string via HMAC-SHA256 with the partner_key.
Setting up variable for signature creation:
$partner_id = 123456; //app_id of Shopee App
$api_path = '/api/v2/shop/get_shop_info'; //shopee API path
$timestamp = time(); //UNIX format of time, 5mins expiration time
$access_token = '...token'; //access_token from /api/v2/shop/auth_partner
shop_id = '654321' //unique shopee shop id
Creating the signature
$tokenBaseString = $partner_id.$api_path.$timestamp.$access_token.$shop_id;
$sign = hash_hmac('sha256', $tokenBaseString, $partner_key, false);
You can now use this signature to create a GET request from Shopee Open Platform
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://partner.shopeemobile.com/api/v2/shop/get_shop_info?access_token=$access_token&partner_id=$partner_id&shop_id=$shop_id&sign=$sign&timestamp=$timestamp',
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(
'Content-Type: application/json'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
Take note that you cannot reuse a signature for other API calls other than what is in the $api_path variable, within the timespan of 5mins from the set $timestamp, you'll need to create another signature after the timespan expires.
Note 2* Always check the API Documentation for generating signature, there are different types of parameters for generating a signature.
Note 3* Open API v1.0 will be deprecate by the end of 2022.

Is it possible to use a server (using PHP) to send an IOS Push Notification?

I am trying to send push notifications using a web server and PHP. I have tried to use the code from an answer to a similar question (https://stackoverflow.com/a/45676540/11051450), but I cannot make it work for some reason. I have figured that this might be because this code was not ment for a web server. In the linked answer they also emphesize that you need curl version 7.38.0, but I think my web server only have curl version 7.29.0. This is the code that I have tried to use:
<?php
$keyfile = 'AuthKey_AABBCC1234.p8'; # <- Your AuthKey file
$keyid = 'AABBCC1234'; # <- Your Key ID
$teamid = 'AB12CD34EF'; # <- Your Team ID (see Developer Portal)
$bundleid = 'com.company.YourApp'; # <- Your Bundle ID
$url = 'https://api.development.push.apple.com'; # <- development url, or use http://api.push.apple.com for production environment
$token = 'e2c48ed32ef9b018........'; # <- Device Token
$message = '{"aps":{"alert":"Hi there!","sound":"default"}}';
$key = openssl_pkey_get_private($keyfile);
$header = ['alg'=>'ES256','kid'=>$keyid];
$claims = ['iss'=>$teamid,'iat'=>time()];
$header_encoded = base64($header);
$claims_encoded = base64($claims);
$signature = ''; //should I put a password here?
openssl_sign($header_encoded . '.' . $claims_encoded, $signature, $key, 'sha256');
$jwt = $header_encoded . '.' . $claims_encoded . '.' . base64_encode($signature);
// only needed for PHP prior to 5.5.24
if (!defined('CURL_HTTP_VERSION_2_0')) {
define('CURL_HTTP_VERSION_2_0', 3);
}
$http2ch = curl_init();
curl_setopt_array($http2ch, array(
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
CURLOPT_URL => "$url/3/device/$token",
CURLOPT_PORT => 443,
CURLOPT_HTTPHEADER => array(
"apns-topic: {$bundleid}",
"authorization: bearer $jwt"
),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $message,
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_TIMEOUT => 30,
CURLOPT_HEADER => 1
));
$result = curl_exec($http2ch);
if ($result === FALSE) {
throw new Exception("Curl failed: ".curl_error($http2ch));
}
$status = curl_getinfo($http2ch, CURLINFO_HTTP_CODE);
echo $status;
function base64($data) {
return rtrim(strtr(base64_encode(json_encode($data)), '+/', '-_'), '=');
}
?>
So my question is as the title explains if you are able to send a push notification to a IOS device using a web sever and if can give me a hint to how I should do it/is doing wrong?

PHP AMAZON API GATEWAY : The request signature we calculated does not match the signature you provided

I want to get response from website content.geappliances.io using AWS API Gateway but always get error :
{"message":"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.\n\nThe Canonical String for this request should have been\n'GET\n/search/b2b/results\n\ncontent-type:\nhost:content.geappliances.io\nx-amz-date:20170401T041050Z\n\ncontent-type;host;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\n20170401T041050Z\n20170401/us-east-1/execute-api/aws4_request\na8aec6a82c0f9a2471bd17faa5f2ce5cd810a16f129fca9b04347ceed54fdc61'\n"}
please somebody help me, what's wrong with my code. Here's the code :
function signRequest(){
$method ='GET';
$uri = '/search/b2b/results?N=0&Ntk=SKU&Ntt=GTX33EASKWW';
$access_key = 'my-access-key';
$secretKey = 'my-secret-key';
$region = 'us-east-1';
$kService = 'execute-api';
$service = 'execute-api';
$options = array();
$headers = array();
$host = "content.geappliances.io";
$alg = 'sha256';
$date = new DateTime( 'UTC' );
$dd = $date->format( 'Ymd\THis\Z' );
$amzdate2 = new DateTime( 'UTC' );
$amzdate2 = $amzdate2->format( 'Ymd' );
$amzdate = $dd;
$algorithm = 'AWS4-HMAC-SHA256';
$requestPayload = "UNSIGNED_PAYLOAD";
$hashedPayload = hash($alg, $requestPayload);
$canonical_uri = $uri;
$canonical_querystring = '';
$canonical_headers = "content-type:"."application/json"."\n"."host:".$host."\n"."x-amz-date:".$amzdate."\n";
$signed_headers = 'content-type;host;x-amz-date';
$canonical_request = "".$method."\n".$canonical_uri."\n".$canonical_querystring."\n".$canonical_headers."\n".$signed_headers."\n".$hashedPayload;
$credential_scope = $amzdate2 . '/' . $region . '/' . $service . '/' . 'aws4_request';
$string_to_sign = "".$algorithm."\n".$amzdate ."\n".$credential_scope."\n".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 = array(
'content-typeapplication/json',
"cache-control: no-cache",
"host: content.geappliances.io",
'x-amz-date: '.$amzdate.'',
'Authorization: '.$authorization_header.''
);
return $headers;
}
$curl = curl_init();
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt_array($curl, array(
CURLOPT_URL => "https://content.geappliances.io/search/b2b/results?N=0&Ntk=SKU&Ntt=GTX33EASKWW",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => signRequest(),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
in the postman, I generate my access key and secret key then it works nicely.
postman result
Take a look at your code
$access_key = 'my-access-key';
$secretKey = 'my-secret-key';
You have to enter your values for access key and secret key there. I assume Amazon provided them for you?
Because your error message says
The request signature we calculated does not match the signature you
provided. Check your AWS Secret Access Key and signing method.

Categories