Focuses on API authentication, access scopes, and permission management.
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.
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'); ?>
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×tamp=... Computed Signature: <computed signature> Signature Verification Failed
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!