HMAC verification issues

I’m trying to validate the requests coming from Shopify but I’m unable to do so.

Here’s the code I’m using to get the hmac hash from the request (from here):

A protocol=http:// or https:// needs to be added to the message

$params = $request;
    
unset($params['signature']);
unset($params['hmac']);

$collected = array_map(function($key, $value) {
    return $key . "=" . $value;
}, array_keys($params), $params);

asort($collected);
$collected = implode('&', $collected);

$shared_secret = 'MySecretHere';
return hash_hmac('sha256', $collected, $shared_secret);

I’m even following the example provided in https://docs.shopify.com/api/authentication/oauth#verification

$message = 'shop=some-shop.myshopify.com&timestamp=1337178173';
$secret = "hush";
var_dump(hash_hmac('sha256', $message, $secret), "2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2"); die();

the hash resulting from that code is: c2812f39f84c32c2edaded339a1388abc9829babf351b684ab797f04cd94d4c7

I have run the same example using ruby (as you suggest on the docs page), creating a file called ‘hmacruby’ with:

require 'openssl'

digest = OpenSSL::Digest.new('sha256')
secret = "hush"
message = "shop=some-shop.myshopify.com&timestamp=1337178173"

digest = OpenSSL::HMAC.hexdigest(digest, secret, message)

p digest

using the command line went to the same folder where that file is stored in and run:

$ ruby hmacruby

the result was: c2812f39f84c32c2edaded339a1388abc9829babf351b684ab797f04cd94d4c7

I also have tested the algorithm here and I’m having the same result. What is going on?

SOLUTION:

As Zelf is pointing out here:

post app install, Shopify will no longer send the “code” parameter that the docs refer to. In fact now they send a “protocol” parameter, which is not in the docs. However, the docs are still correct, you need to grab all parameters and remove signature and hmac, then whatever is left over, simply sort lexicographically, then hash_hmac with secret token like in the code example above.

After install, adding "protocol=https://&"; to the beginning of the message works. so the entire message NEEDS TO HAVE every param received from Shopify + the protocol (which should be 'https://'), which should be added first thing.

"protocol=https://&shop=yourshop.myshopify.com&timestamp=1439924556"; including ‘protocol=’

3 Likes

Hi Shayne!

full request:

array(3) {
["shop"]=> string(31) "my-devstore.myshopify.com"
["hmac"]=> string(64) "7e5bed48e9a7773204cbad9052993e7ddf837593b878e3a4208f597651b0a9fb"
["timestamp"]=> string(10) "1439924556"
}

what I’m trying to validate: “shop=praxisis-devstore.myshopify.com&timestamp=1439924556”

shopify’s hmac: 7e5bed48e9a7773204cbad9052993e7ddf837593b878e3a4208f597651b0a9fb

my hmac: ebf1aecf5e396905c0b1af69f058390491d99f9af2508633661e483e2e03015a

Thanks!

XXXXXXX is the app name.

I’m using the one you need to click on the input field to see it (it says ‘show secret’ otherwise) Starts with “xxxx”.

Running:

require 'openssl'

digest = OpenSSL::Digest.new('sha256')
secret = "xxxxTHERESTOFTHEKEY"
message = "shop=my-devstore.myshopify.com&timestamp=1439924556"

digest = OpenSSL::HMAC.hexdigest(digest, secret, message)

p digest

I’m getting the same hash I get with php: ebf1aecf5e396905c0b1af69f058390491d99f9af2508633661e483e2e03015a

Adding "protocol=https://&"; to the begging of the message works. so the entire message NEEDS TO BE

"protocol=https://&shop=praxisis-devstore.myshopify.com&timestamp=1439924556"; including ‘protocol=’

Somebody needs to update this page, I’ve wasted half of my day dealing with this issue.

Thank you Shayne.

4 Likes

Nothing works for me atm. I get a return map like:

            [url] => shopify/callback
            [code] => 195fdf64272c1cb25e6008dcacf17584
            [hmac] => 346308335956bd1ed4dfb9ce514502cf61d122e76e754faa44263147b6d05678
            [shop] => bright-therapy-shopify-2016.myshopify.com
            [signature] => 95ca7fc2a3cdf32b288d75c929af2bae
            [state] => 1234560
            [timestamp] => 1448916681

I’ve removed hmac & signature, sorted it out, concatenated etc. but the return hmac is still different.

Worked for me without adding

protocol=[https://&](https://&amp);

Useful thread in any case. The example provided in the API docs makes it seem as though you only have to pass the “shop” and “timestamp” values. You also have to include the “code” and “state” query string values. I’m guessing that the example is outdated.

Hi Dan, are you getting the same return map as me? and can you share your url combine? thank you.

Hi Mark, Try without sorting the params.

The API explains neatly what should be done. We should not interpret the logic just from the example provided. People are mis-interpretting with the example provided that "only shop and timestamp are needed to compute hmac". That is not the case. Go back and read the API specification again and forget the example.

Below code works for me

function compute_hmac($request_vars ){

	if (isset($request_vars['signature'])) {
		unset($request_vars['signature']);	
	}
	
	if (isset($request_vars['hmac'])) {
		unset($request_vars['hmac']);	
	}
	$compute_array = array();

	foreach ($request_vars as  $k => $val) {
		$k = str_replace('%', '%25', $k);
		$k = str_replace('&', '%26', $k);	
		$k = str_replace('=', '%3D', $k);
		$val = str_replace('%', '%25', $val);
		$val = str_replace('&', '%26', $val);	
		$compute_array[$k] = $val;
	}

	$message = http_build_query($compute_array);
	$key = Config::get('app.shopify_client_secret');
	
	$digest = hash_hmac ( 'sha256' , $message , $key , false ) ; 

	return $digest;
}
1 Like

Just struggled with this but got it figured out.

			$queryString = http_build_query(array('code' => $da['code'], 'shop' => $da['shop'], 'state' => $da['state'], 'timestamp' => $da['timestamp']));
			$match = $da['hmac'];
			$calculated = hash_hmac('sha256', $queryString, $this->_API['API_SECRET']);
			$queryString = http_build_query(array('code' => $da['code'], 'shop' => $da['shop'], 'state' => $da['state'], 'timestamp' => $da['timestamp']));
			$match = $da['hmac'];
			$calculated = hash_hmac('sha256', $queryString, $this->_API['API_SECRET']);

Hi sasi varna kumar,

I don’t know if you’ve read the documentation properly. To quote, it actually says “The list of key-value pairs is sorted lexicographically, and concatenated together with”.

Still, not working for me. So I’ve preferred using MD5. That works. I don’t care if it stops on June. Since nobody cares.

Regards,

Mark

1 Like

This worked well and pointed out the failure of the documentation on the process.

The docs on HMAC validation really should be adjusted to the current reality because they are incorrect.

Hi guys,

Please be aware that MD5 signature validation is now deprecated as of June 1st as per this announcement.

The example provided in the OAuth documentation is not meant to be representative of all requests sent by Shopify, and the parameters are subject to change. The important steps to follow for HMAC signature validation are the following:

  1. Retrieve **all** of the request parameters sent from Shopify (not just the parameters shown in the example)
  2. Convert the parameters to a hash (or similar data structure)
  3. Remove the hmac parameter from the hash.
  4. Sort the keys lexographically (each key-value pair is joined by an ‘=’ character) and concatenate the key-value pairs with an ampersand(‘&’).
  5. Calculate the SHA256 hash of this digest using the application’s secret key as the encryption key.

Hope this helps. If you continue to experience any issues with HMAC validation, please reply to this thread.

hi Jamie,

thank you for responding back. I mean, have you made a very simple test in PHP? Did you tried your process on validating through HMAC? If not man, I hope we can at least follow through.

Hi Jamie,I have used below code in php,

$request_vars = array('code' => $code, 'shop' => $shop, 'timestamp' => $timestamp);

$compute_array = array();

foreach ($request_vars as  $k => $val) {
	$k 	 = str_replace('%', '%25', $k);
	$k 	 = str_replace('&', '%26', $k);	
	$k 	 = str_replace('=', '%3D', $k);
	$val = str_replace('%', '%25', $val);
	$val = str_replace('&', '%26', $val);	
	$compute_array[$k] = $val;
}

$message = http_build_query($compute_array);
$key 	 = 'MY SECRET';
		
$digest = hash_hmac('sha256' , $message , $key ); 

But still it is giving wrong hmac. Any idea why ?

Here is the PHP class I am wrote for hmac calculation that has been working for us. Note there is a different signing method when calculating the hmac for a webhook, simply use the
signRequestData method when handling the authentication flow. When passing data in to the webhook signer method you should use

file_get_contents('php://input'))

When passing data into the signRequestData method you sould pass in all of the request params, use the super global $_REQUEST or your frameworks Request::all() method

<?php

class ShopifyRequestSigner
{

    /**
     * @var string
     */
    private $secretKey;

    /**
     * ShopifyRequestSigner constructor.
     *
     * @param $secretKey
     */
    public function __construct($secretKey)
    {
        $this->secretKey = $secretKey;
    }

    /**
     * @return mixed
     */
    public function signWebhookRequestData($data)
    {
        return base64_encode(hash_hmac('sha256', $data, $this->secretKey, true));
    }

    /**
     * @param array $data
     *
     * @return string
     */
    public function signRequestData(array $data)
    {
        $data = $this->stripSignatureAndHmac($data);

        $dataString = $this->getString($data);

        return hash_hmac('sha256', $dataString, $this->secretKey);
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private function stripSignatureAndHmac(array $data)
    {
        unset($data['signature'], $data['hmac']);

        return $data;
    }

    /**
     * @param array $data
     *
     * @return string
     */
    private function getString(array $data)
    {
        $ret = '';

        $encodedData = $this->encodeData($data);

        $sortedData = $this->sortData($encodedData);

        foreach ($sortedData as $key => $value) {

            $value = is_array($value) ? $this->getString($value) : $value;

            $ret .= $key . '=' . $value . '&';
        }

        return rtrim($ret, '&');
    }

    /**
     * @param array $data
     *
     * @return array
     */
    private function encodeData(array $data)
    {
        $encoded = [];

        foreach ($data as $key => $value) {

            $encoded[$this->encodeKey($key)] = $this->encodeValue($value);
        }

        return $encoded;
    }

    /**
     * @param $string
     *
     * @return string
     */
    private function encodeValue($string)
    {
        return $this->encodeString($string);
    }

    /**
     * @param $string
     *
     * @return mixed
     */
    private function encodeKey($string)
    {
        $encoded = $this->encodeString($string);

        return str_replace('=', '%3D', $encoded);
    }

    /**
     * @param $string
     *
     * @return string
     */
    private function encodeString($string)
    {
        return str_replace(['&', '%'], ['%26', '%25'], $string);
    }

    /**
     * @param array $encodedData
     *
     * @return array
     */
    private function sortData(array $encodedData)
    {
        ksort($encodedData);

        return $encodedData;
    }
}

Hey all,

If you’re still having difficulties, the examples posted by sasi and jcrowe should help.

Please make sure that you’re computing the digest using all of the parameters sent by Shopify. You should not be hardcoding any parameters as they will not be the same in all cases.

Cheers,

Jamie
Developer Support

Hello Jamie,

Hmac matching doesn’t work when I send state variable as below

'state='+encodeURIComponent('security_token=***&&url=[http://redirect.mydomain.com/shopify.php](http://redirect.mydomain.com/shopify.php)')

And I do this becuase my application is a SAAS based, where domains are dynamic and I can not set all domains in App redirect uri. And there is only state variable which comes back as it is. So I added token and an url in that vairable. But in this case hmac calulation doesn’t work.

Any idea why ?

Make sure you don’t have two API secret keys in partner central, and if you do, that you’re validating it against the correct one.