Shopify App Proxy: Unauthorized Response After Signature Verification Fails

Topic summary

A developer is encountering persistent 401 “Unauthorized” errors when implementing a Shopify app proxy, despite believing their HMAC signature verification is correctly configured.

Core Problem:

  • Signature verification consistently fails, blocking access to the proxy endpoint
  • Database connection tests pass successfully
  • The computed HMAC doesn’t match the signature sent in requests

Troubleshooting Steps Taken:

  • Verified SHOPIFY_APP_SECRET matches Partner Dashboard credentials
  • Confirmed proper use of hash_hmac('sha256', $encoded_string, SHOPIFY_APP_SECRET)
  • Ensured query parameters are sorted and encoded per Shopify documentation
  • Logged $_GET array, computed signatures, and encoded strings for debugging
  • Confirmed HTTPS is being used for proxy requests

Code Details:
The provided PHP proxy script includes error logging, signature verification logic, database queries to fetch shop data, and conditional page rendering based on request parameters. Notably, portions of the code appear corrupted or reversed in the submission.

Status: The issue remains unresolved, with the developer seeking community assistance to identify why signature verification fails despite following documented procedures.

Summarized with AI on November 5. AI used: claude-sonnet-4-5-20250929.

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?

  1. Could my encoding logic for generating the HMAC signature be incorrect? Specifically, should I handle any parameters differently (like empty values)?
  2. 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?
  3. 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!