Sanhita Sinha
Sanhita Sinha

Reputation: 31

shopify hmac verification php

This is my code :

function verifyRequest($request, $secret) {
  // Per the Shopify docs:
  // Everything except hmac and signature...

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

  // Sorted lexilogically...
  ksort($request);

  // Special characters replaced...
  foreach ($request 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);
    $params[$k] = $val;
  }

  echo $http = "protocol=". urldecode("https://").http_build_query( $params) ;
  echo $test = hash_hmac("sha256", $http , $secret);

  // enter code hereVerified when equal
  return $hmac === $test;
}

The hmac from shopi and hmac created from my code is not matching.

What am I doing wrong?

Upvotes: 2

Views: 4429

Answers (5)

Mahbub Ansary
Mahbub Ansary

Reputation: 1

$hmac = $_GET['hmac'];         
$str = http_build_query([
    'host' => $_GET['host'],
    'shop' => $_GET['shop'],
    'timestamp' => $_GET['timestamp']
]);
$ver_hmac =  hash_hmac('sha256',$str,'api_secret', false);
return $ver_hmac === $hmac;

It requires these 3 parameters to verify the hmac - (host,shop,timestamp)

Upvotes: 0

Sam Critchley
Sam Critchley

Reputation: 3778

I've got the following to do this:

// Remove the 'hmac' parameter from the query string
$query_string_rebuilt = removeParamFromQueryString($_SERVER['QUERY_STRING'], 'hmac');
// Check the HMAC
if(!checkHMAC($_GET['hmac'], $query_string_rebuilt, $shopify_api_secret_key)) {
    // Error code here
}

/**
 * @param string $comparison_data
 * @param string $data
 * @param string $key
 * @param string $algorithm
 * @param bool $binary
 * @return bool
 */
function checkHMAC($comparison_data, $data, $key, $algorithm = 'sha256', $binary=false) {
    // Check the HMAC
    $hash_hmac = hash_hmac($algorithm, $data, $key, $binary);
    // Return true if there's a match
    if($hash_hmac === $comparison_data) {
        return true;
    }
    return false;
}

/**
 * @param string $query_string
 * @param string $param_to_remove
 * @return string
 */
function removeParamFromQueryString(string $query_string, string $param_to_remove) {
    parse_str($query_string, $query_string_into_array);
    unset($query_string_into_array[$param_to_remove]);
    return http_build_query($query_string_into_array);
}

Upvotes: 0

Max S.
Max S.

Reputation: 1461

Notice for other requests like the App Proxy a HMAC will not be preset, so you'll need to calculate the signature. Here a function that caters for both types of requests including webhooks:

public function authorize(Request $request)
{
    if( isset($request['hmac']) || isset($request['signature']) ){
        try {
            $signature = $request->except(['hmac', 'signature']);

            ksort($signature);

            foreach ($signature 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);
                $signature[$k] = $val;
            }

            if(isset($request['hmac'])){
                $test = hash_hmac('sha256', http_build_query($signature), env('SHOPIFY_API_SECRET'));

                if($request->input('hmac') === $test){
                    return true;
                }
            } elseif(isset($request['signature'])){
                $test = hash_hmac('sha256', str_replace('&', '', urldecode(http_build_query($signature))), env('SHOPIFY_API_SECRET'));

                if($request->input('signature') === $test){
                    return true;
                }
            }
        } catch (Exception $e) {
            Bugsnag::notifyException($e);
        }
    } else { // If webhook
        $calculated_hmac = base64_encode(hash_hmac('sha256', $request->getContent(), env('SHOPIFY_API_SECRET'), true));

        return hash_equals($request->server('HTTP_X_SHOPIFY_HMAC_SHA256'), $calculated_hmac);
    }

    return false;
}

The above example uses some Laravel functions, so уоu may want to replace them if you use a different framework.

Upvotes: 0

Viral
Viral

Reputation: 1337

hmac can be calculated in any programming language using sha256 cryptographic algorithm.

However the doc for hmac verification is provided by shopify but still there is confusion among app developers how to implement it correctly.

Here is the code in php for hmac verification. Ref. http://code.codify.club

<?php

function verifyHmac()
{
  $ar= [];
  $hmac = $_GET['hmac'];
  unset($_GET['hmac']);

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

    $key=str_replace("%","%25",$key);
    $key=str_replace("&","%26",$key);
    $key=str_replace("=","%3D",$key);
    $value=str_replace("%","%25",$value);
    $value=str_replace("&","%26",$value);

    $ar[] = $key."=".$value;
  }

  $str = join('&',$ar);
  $ver_hmac =  hash_hmac('sha256',$str,"YOUR-APP-SECRET-KEY",false);

  if($ver_hmac==$hmac)
  {
    echo 'hmac verified';
  }

}
?>

Upvotes: 1

Nicky Woolf
Nicky Woolf

Reputation: 101

You only need to include the request parameters when creating the list of key-value pairs - don't need "protocol=https://".

https://help.shopify.com/api/getting-started/authentication/oauth#verification

You'll need to urldecode() the result of http_build_query(). It returns a url-encoded query string.

http://php.net/manual/en/function.http-build-query.php

Instead of:

 echo $http = "protocol=". urldecode("https://").http_build_query( $params) ;
 echo $test = hash_hmac("sha256", $http , $secret);

Something like this:

 $http = urldecode(http_build_query($params));
 $test = hash_hmac('sha256', $http, $secret);

Upvotes: 2

Related Questions