Issues calculating signature for Amazon Marketplace API
Asked Answered
A

2

20

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.

Avaricious answered 16/4, 2015 at 15:41 Comment(0)
A
8

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;
    }

}
Avaricious answered 18/4, 2015 at 23:27 Comment(0)
L
2

Try this function after calling your sign function:

  function amazonEncode($text)
  {
    $encodedText = "";
    $j = strlen($text);
    for($i=0;$i<$j;$i++)
    {
      $c = substr($text,$i,1);
      if (!preg_match("/[A-Za-z0-9\-_.~]/",$c))
      {
        $encodedText .= sprintf("%%%02X",ord($c));
      }
      else
      {
        $encodedText .= $c;
      }
    }
    return $encodedText;
  }

Reference

After you've created the canonical string as described in Format the Query Request, you calculate the signature by creating a hash-based message authentication code (HMAC) using either the HMAC-SHA1 or HMAC-SHA256 protocols. The HMAC-SHA256 protocol is preferred.

The resulting signature must be base-64 encoded and then URI encoded.

Limoges answered 18/4, 2015 at 21:0 Comment(3)
I’m not sure what the body of your amazonEncode() method is doing, but I seem to be doing what the documentation says in my sign() method, but using the base64_encode and hash_hmac functions. Can you explain the difference? Also, the documentation you provided is for AWS, not MWS.Avaricious
It's similar, the signature generation for both of them. You will need to URI encode the generated base64, which is almost what my amazonEncode does.Limoges
I’ve wrapped the result of my sign() method in your amazonEncode() function, but I’m still getting SignatureDoesNotMatch error.Avaricious

© 2022 - 2024 — McMap. All rights reserved.