Our Partner & Developer boards on the community are moving to a brand new home: the .dev community forums! While you can still access past discussions here, for all your future app and storefront building questions, head over to the new forums.

Shopify App Proxy: Unauthorized Response After Signature Verification Fails

Shopify App Proxy: Unauthorized Response After Signature Verification Fails

Lynn89
Shopify Partner
4 0 0

Hi Shopify Partner Community,

I’m currently developing a Shopify-embedded app using an app proxy to render dynamic content. I'm facing an issue where the proxy page consistently returns a 401 "Unauthorized" response, even though my signature verification logic appears correct.

Issue Overview:

  • I have a Shopify app set up with the app proxy endpoint.
  • The database connection test passes successfully.
  • However, signature verification always fails, resulting in an unauthorized response.
  • Below is a summary of my setup and the full PHP code for my proxy script.

Debugging Attempts So Far:

  • I am using hash_hmac('sha256', $encoded_string, SHOPIFY_APP_SECRET) to compute the HMAC signature.
  • Verified that the SHOPIFY_APP_SECRET in the code matches what is in the Shopify Partner Dashboard.
  • The encoded query string parameters are sorted and hashed correctly, as per the documentation.
  • Logged the $_GET array, computed signatures, and the encoded string used in the HMAC calculation to debug, but I can't see why the computed HMAC is different from the one sent in the request.
  • I have ensured the request to the proxy URL is going through https.

PHP Code (proxy.php):

Here is the complete code for my proxy script:

 

<?php
// Skip Ngrok browser warning
header("ngrok-skip-browser-warning: true");

// Enable error reporting to diagnose issues
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Start output buffering
ob_start();

// Set up error logging
ini_set('log_errors', 1);
ini_set('error_log', 'debug.log');

// Custom error handler for logging errors
function custom_error_handler($errno, $errstr, $errfile, $errline) {
    $message = date('Y-m-d H:i:s') . " - Error [$errno] $errstr in $errfile on line $errline\n";
    error_log($message, 3, 'debug.log');
}
set_error_handler("custom_error_handler");

// Log the start of the script and incoming request
error_log('--- New Request ---', 3, 'debug.log');
error_log('REQUEST: ' . print_r($_REQUEST, true), 3, 'debug.log');

include_once("includes/mysql_connect.php");
include_once("includes/shopify.php");

// Test database connection
echo "Testing database connection<br>";
if ($mysql->ping()) {
    echo "Database connection successful<br>";
} else {
    echo "Database connection failed: " . $mysql->error . "<br>";
}

// Shopify app secret
define('SHOPIFY_APP_SECRET', '********************************');

// Verify the request is coming from Shopify
$query_string = $_SERVER['QUERY_STRING'];
$signature = isset($_GET['signature']) ? $_GET['signature'] : '';
error_log('Query String: ' . $query_string, 3, 'debug.log');
error_log('Signature from request: ' . $signature, 3, 'debug.log');

// Remove signature from query string
parse_str($query_string, $params);
unset($params['signature']);

// Sort parameters lexicographically
ksort($params);

// Construct the string to hash
$encoded_params = [];
foreach ($params as $key => $value) {
    $encoded_key = rawurlencode($key);
    $encoded_value = rawurlencode($value);
    $encoded_params[] = $encoded_key . '=' . $encoded_value;
}
$encoded_string = implode('&', $encoded_params);

error_log('SHOPIFY_APP_SECRET: ' . substr(SHOPIFY_APP_SECRET, 0, 5) . '...', 3, 'debug.log');
error_log('$_GET: ' . print_r($_GET, true), 3, 'debug.log');
error_log('$params after sorting: ' . print_r($params, true), 3, 'debug.log');
error_log('Encoded string for hashing: ' . $encoded_string, 3, 'debug.log');

// Compute the HMAC signature
$computed_signature = hash_hmac('sha256', $encoded_string, SHOPIFY_APP_SECRET);
error_log('Computed Signature: ' . $computed_signature, 3, 'debug.log');

// Verify the signature
if (hash_equals($signature, $computed_signature)) {
    error_log('Signature Verification Successful', 3, 'debug.log');

    // Request is verified
    $page = isset($_GET['page']) ? $_GET['page'] : 'default';
    $shop = isset($_GET['shop']) ? $_GET['shop'] : '';
    error_log('Requested Page: ' . $page, 3, 'debug.log');
    error_log('Shop: ' . $shop, 3, 'debug.log');
    
    // Fetch shop data from your database
    $query = "SELECT * FROM shops WHERE shop_url = '" . $mysql->real_escape_string($shop) . "' LIMIT 1";
    $result = $mysql->query($query);

    if ($result === false) {
        error_log('MySQL Error: ' . $mysql->error, 3, 'debug.log');
    } else {
        $store_data = $result->fetch_assoc();
        error_log('Store Data: ' . print_r($store_data, true), 3, 'debug.log');

        if ($store_data) {
            $shopify = new Shopify();
            $shopify->set_url($shop);
            $shopify->set_token($store_data['access_token']);

            // Render the appropriate page based on the request
            switch ($page) {
                case 'custom-page':
                case 'custom-page.php':
                    include('frontend/custom-page.php');
                    break;
                case 'default-page':
                case 'default-page.php':
                    include('frontend/default-page.php');
                    break;
                default:
                    echo "Page not found";
                    error_log('Page not found: ' . $page, 3, 'debug.log');
                    break;
            }
        } else {
            error_log('Store not found in database', 3, 'debug.log');
            echo "Store not found in database";
        }
    }
} else {
    // Signature verification failed
    error_log('Signature Verification Failed', 3, 'debug.log');
    header("HTTP/1.1 401 Unauthorized");
    echo "Unauthorized";
}

// Log the output and end the request
$output = ob_get_contents();
error_log('OUTPUT: ' . $output, 3, 'debug.log');
ob_end_flush();
error_log('--- End of Request ---', 3, 'debug.log');
?>

Debug Log Output:

Here's an example of the debug log output I'm seeing:

--- New Request ---
REQUEST: Array
(
    [page] => custom-page.php
    [shop] => checkoutlet.myshopify.com
    ...
)
Query String: page=custom-page.php&shop=checkoutlet.myshopify.com&...
Signature from request: <request signature>
SHOPIFY_APP_SECRET: 70218...
$params after sorting: Array
(
    ...
)
Encoded string for hashing: logged_in_customer_id=&page=custom-page.php&path_prefix=/apps/app-proxy&shop=checkoutlet.myshopify.com&timestamp=...
Computed Signature: <computed signature>
Signature Verification Failed

Console Output:

  • Testing database connection: success
  • Unauthorized (401)
  • Console logs show a 401 Unauthorized error.

Questions:

  1. Are there any recent changes to the Shopify app proxy HMAC signature verification process that could explain why my computed signature doesn't match the one sent by Shopify?
  2. Could my encoding logic for generating the HMAC signature be incorrect? Specifically, should I handle any parameters differently (like empty values)?
  3. I have tested with both 'signature' and 'hmac' as the parameter key for verification, but neither seems to work. Could there be an inconsistency in how Shopify sends these values for the app proxy?
  4. Should any extra headers be included to make this work correctly?

I would really appreciate any insights or examples from anyone who has successfully set up a similar app proxy recently. I'm at a point where I'm not sure if it's my implementation, the configuration, or an API change.

Thanks in advance!

 

Replies 0 (0)