skip to Main Content

When a client installs the app, they have the option to click on the app name in the list of apps on the /admin/apps page.

When they click that page, my PHP index file for my app receives these $_GET vars:

hmac = some_long_alphanumaeric_hmac
locale = en
protocol = https://
shop = example-shop.myshopify.com
timestamp = 1535609063

To verify a webhook from Shopify, I successfully use this:

function verify_webhook($data, $hmac_header, $app_api_secret) {
    $calculated_hmac = base64_encode(hash_hmac('sha256', $data, $app_api_secret, true));
    return ($hmac_header == $calculated_hmac);
}

// Set vars for Shopify webhook verification
$hmac_header = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'];
$data = file_get_contents('php://input');
$verified = verify_webhook($data, $hmac_header, MY_APP_API_SECRET);

Is it possible to verify an app admin page visit is from a Shopify client that has the app installed?

PS: I’ve looked through both, the Embedded Apps API (but I can’t figure out if that’s even the right documentation or if I’m doing something wrong), as well as the GitHub example provided (which has no instructions on how to verify an Embedded App admin page visit).

UPDATE:

I’ve tried various other ways, discovering some ridiculous problems along the way, but still no luck.

  1. The method I understand should be used to verify a Shopify HMAC is something akin to this:

    function verify_hmac($hmac = NULL, $shopify_app_api_secret) {
        $params_array = array();
        $hmac = $hmac ? $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);
            $params_array[] = $key . "=" . $value;
        }
    
        $params_string = join('&', $params_array);
        $computed_hmac = hash_hmac('sha256', $params_string, $shopify_app_api_secret);
    
        return hash_equals($hmac, $computed_hmac);
    }
    

But the line $params_string = join('&', $params_array); causes an annoying problem by encoding &timestamp as xtamp … Using http_build_query($params_array) results in the same ridiculous thing. Found others having this same problem here. Basically resolved by encoding the & as &, to arrive at $params_string = join('&', $params_array);.

  1. My final version is like this, but still doesn’t work (all the commented code is what else I’ve tried to no avail):

    function verify_hmac($hmac = NULL, $shopify_app_api_secret) {
        $params_array = array();
        $hmac = $hmac ? $hmac : $_GET['hmac'];
        unset($_GET['hmac']);
    //  unset($_GET['protocol']);
    //  unset($_GET['locale']);
    
        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);
            $params_array[] = $key . "=" . $value;
    //  This commented out method below was an attempt to see if 
    //  the imporperly encoded query param characters were causing issues
    /*
            if (!isset($params_string) || empty($params_string)) {
                $params_string = $key . "=" . $value;
            }
            else {
                $params_string = $params_string . "&" . $key . "=" . $value;
            }
    */
        }
    
    //  $params_string = join('&', $params_array);
    //  echo $params_string;
    //  $computed_hmac =  base64_encode(hash_hmac('sha256', $params_string, $shopify_app_api_secret, true));
    //  $computed_hmac =  base64_encode(hash_hmac('sha256', $params_string, $shopify_app_api_secret, false));
    //  $computed_hmac =  hash_hmac('sha256', $params_string, $shopify_app_api_secret, false);
    //  $computed_hmac =  hash_hmac('sha256', $params_string, $shopify_app_api_secret, true);
        $computed_hmac = hash_hmac('sha256', http_build_query($params_array), $shopify_app_api_secret);
    
        return hash_equals($hmac, $computed_hmac);
    }
    

3

Answers


  1. If you get a hit from Shopify, the first thing you do is check in your persistence layer if you have the shop registered. If you do, and you have a session of some kind setup, you are free to render your App to that shop. If you do not have the shop persisted, you go through the oAuth cycle to get an authentication token to use on the shop, which you persist along with the shop and new session.

    For any routes or end points in your shop where you are receiving webhooks, of course those requests have no session, so you use the HMAC security approach to figure out what to do. So your question is clearly straddling two different concepts, each handled differently. The documentation is pretty clear on the differences.

    Login or Signup to reply.
  2. Here is the relevant documentation: https://shopify.dev/tutorials/authenticate-with-oauth#verification. This info by Sandeep was also very helpful too: https://community.shopify.com/c/Shopify-APIs-SDKs/HMAC-verify-app-install-request-using-php/m-p/140097#comment-253000.

    Here is what worked for me:

    function verify_visiter() // returns true or false
    {
      // check that timestamp is recent to ensure that this is not a 'replay' of a request that has been intercepted previously (man in the middle attack)
      if (!isset($_GET['timestamp'])) return false;
        $seconds_in_a_day = 24 * 60 * 60;
        $older_than_a_day = $_GET['timestamp'] < (time() - $seconds_in_a_day);
        if ($older_than_a_day) return false;
      $shared_secret = Your_Shopify_app_shared_secret;
      $hmac_header = $_GET['hmac'];
      unset($_GET['hmac']);
      $data = urldecode(http_build_query($_GET));
      $calculated_hmac = hash_hmac('sha256', $data, $shared_secret, false);
      return hash_equals($hmac_header, $calculated_hmac);
    }
    
    $verified = verify_visiter();
    
    if (!$verified) {
      exit('User verification failed.');
    }
    
    // ... everything else...
    
    
    Login or Signup to reply.
  3. public function authenticateCalls($data = NULL, $bypassTimeCheck = FALSE)
    {
        $da = array();
        foreach($data as $key => $val)
        {
            $da[$key] = $val; 
        }
        if(isset($da['hmac']))
        {
            unset($da['hmac']);
        }   
                
        ksort($da); 
        
        // Timestamp check; 1 hour tolerance
        if (!$bypassTimeCheck)
        {
            if (($da['timestamp'] - time() > 3600))
            {
                return false; 
            }
        }
        
        // HMAC Validation
        $queryString = http_build_query($da);
        $match = $data['hmac'];
        $calculated = hash_hmac('sha256', $queryString, $this->_API['API_SECRET']);
        
        return $calculated === $match;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search