Search code examples
phpamazon-mwsamazon-marketplace

Issues calculating signature for Amazon Marketplace API


I’m trying to calculate a signature to make Amazon Marketplace API calls, but I keep getting the following error:

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.

I’ve wrapped the signature creation process into a class:

<?php
namespace App\Marketplace\Amazon;

class Signature
{
    protected $signedString;

    public function __construct($url, array $parameters, $secretAccessKey)
    {
        $stringToSign = $this->calculateStringToSign($url, $parameters);

        $this->signedString = $this->sign($stringToSign, $secretAccessKey);
    }

    protected function calculateStringToSign($url, array $parameters)
    {
        $url = parse_url($url);

        $string = "POST\n";
        $string .= $url['host'] . "\n";
        $string .= $url['path'] . "\n";
        $string .= $this->getParametersAsString($parameters);

        return $string;
    }

    protected function sign($data, $secretAccessKey)
    {
        return base64_encode(hash_hmac('sha256', $data, $secretAccessKey, true));
    }

    protected function getParametersAsString(array $parameters)
    {
        uksort($parameters, 'strcmp');

        $queryParameters = [];

        foreach ($parameters as $key => $value) {
            $queryParameters[$key] = $this->urlEncode($value);
        }

        return http_build_query($queryParameters);
    }

    protected function urlEncode($value)
    {
        return str_replace('%7E', '~', rawurlencode($value));
    }

    public function __toString()
    {
        return $this->signedString;
    }
}

But I can’t for the life of me see where I’m going wrong. I’ve followed the guide in the API, and looked at the Java example as well as the antiquated Marketplace PHP SDK*.

EDIT: And here is how I’m using the Signature class:

$version = '2011-07-01';

$url = 'https://mws.amazonservices.com/Sellers/'.$version;

$timestamp = gmdate('c', time());

$parameters = [
    'AWSAccessKeyId' => $command->accessKeyId,
    'Action' => 'GetAuthToken',
    'SellerId' => $command->sellerId,
    'SignatureMethod' => 'HmacSHA256',
    'SignatureVersion' => 2,
    'Timestamp' => $timestamp,
    'Version' => $version,
];

$signature = new Signature($url, $parameters, $command->secretAccessKey);

$parameters['Signature'] = strval($signature);

try {
    $response = $this->client->post($url, [
        'headers' => [
            'User-Agent' => 'my-app-name',
        ],
        'body' => $parameters,
    ]);

    dd($response->getBody());
} catch (\Exception $e) {
    dd(strval($e->getResponse()));
}

As an aside: I know the Marketplace credentials are correct as I’ve logged in to the account and retrieved the access key, secret, and seller IDs.

* I’m not using the SDK as it doesn’t support the API call I need: SubmitFeed.


Solution

  • I’m not sure what I’ve changed, but my signature generation is working now. Below is the contents of the class:

    <?php
    namespace App\Marketplace\Amazon;
    
    class Signature
    {
        /**
         * The signed string.
         *
         * @var string
         */
        protected $signedString;
    
        /**
         * Create a new signature instance.
         *
         * @param  string  $url
         * @param  array   $data
         * @param  string  $secretAccessKey
         */
        public function __construct($url, array $parameters, $secretAccessKey)
        {
            $stringToSign = $this->calculateStringToSign($url, $parameters);
    
            $this->signedString = $this->sign($stringToSign, $secretAccessKey);
        }
    
        /**
         * Calculate the string to sign.
         *
         * @param  string  $url
         * @param  array   $parameters
         * @return string
         */
        protected function calculateStringToSign($url, array $parameters)
        {
            $url = parse_url($url);
    
            $string = "POST\n";
            $string .= $url['host']."\n";
            $string .= $url['path']."\n";
            $string .= $this->getParametersAsString($parameters);
    
            return $string;
        }
    
        /**
         * Computes RFC 2104-compliant HMAC signature.
         *
         * @param  string  $data
         * @param  string  $secretAccessKey
         * @return string
         */
        protected function sign($data, $secretAccessKey)
        {
            return base64_encode(hash_hmac('sha256', $data, $secretAccessKey, true));
        }
    
        /**
         * Convert paremeters to URL-encoded query string.
         *
         * @param  array  $parameters
         * @return string
         */
        protected function getParametersAsString(array $parameters)
        {
            uksort($parameters, 'strcmp');
    
            $queryParameters = [];
    
            foreach ($parameters as $key => $value) {
                $key = rawurlencode($key);
                $value = rawurlencode($value);
    
                $queryParameters[] = sprintf('%s=%s', $key, $value);
            }
    
            return implode('&', $queryParameters);
        }
    
        /**
         * The string representation of this signature.
         *
         * @return string
         */
        public function __toString()
        {
            return $this->signedString;
        }
    
    }