diff --git a/docs/usage/utils.md b/docs/usage/utils.md index d92f8d22..6171bc98 100644 --- a/docs/usage/utils.md +++ b/docs/usage/utils.md @@ -51,6 +51,18 @@ Accepted arguments: This method will return whether the `hmac` key in `params` is valid. +## `validateSignature` + +Determines if a request is valid by checking the HMAC hash received in a request. + +Accepted arguments: +| Parameter | Type | Required | Default Value | Notes | +| --- | --- | :---: | :---: | --- | +| `params` | `array` | Yes | - | Query parameters from a URL | +| `secret` | `string` | Yes | - | The secret key associated with the app in the Partners Dashboard | + +This method will return whether the `signature` key in `params` is valid. + ## `decodeSessionToken` Decodes the given session token (JWT) and extracts its payload, using `Context::$API_SECRET_KEY` as the secret. diff --git a/src/Utils.php b/src/Utils.php index a0fb8c97..96cf77cd 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -78,6 +78,31 @@ public static function validateHmac(array $params, string $secret): bool return hash_equals($hmac, $computedHmac); } + /** + * Determines if request is valid by processing secret key through an HMAC-SHA256 hash function + * + * @param array $params array of parameters parsed from a URL + * @param string $secret the secret key associated with the app in the Partners Dashboard + * + * @return bool true if the generated hexdigest is equal to the signature parameter, false otherwise + */ + public static function validateSignature(array $params, string $secret): bool + { + $signature = $params['signature'] ?? ''; + unset($params['signature']); + + $params = array_map( + fn($k, $v) => "{$k}=" . (is_array($v) ? implode(',', $v) : $v), + array_keys($params), + array_values($params) + ); + asort($params); + + $computedSignature = hash_hmac('sha256', implode($params), $secret);; + + return hash_equals($signature, $computedSignature); + } + /** * Retrieves the query string arguments from a URL, if any * diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index ef4236a0..0d95d12f 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -123,6 +123,44 @@ public function testInvalidHmac() )); } + public function testValidSignature() + { + // phpcs:ignore + $url = 'https://123456.ngrok.io?extra%5B%5D=1&extra%5B%5D=2&shop=some-shop.myshopify.com&path_prefix=%2Fapps%2Fawesome_reviews×tamp=1337178173'; + $params = Utils::getQueryParams($url); + $secret = 'test-secret'; + $sortedParams = 'extra=1,2path_prefix=/apps/awesome_reviewsshop=some-shop.myshopify.comtimestamp=1337178173'; + $params['signature'] = hash_hmac('sha256', $sortedParams, $secret); + + $this->assertEquals(true, Utils::validateSignature( + $params, + $secret + )); + } + + public function testInvalidSignature() + { + // phpcs:ignore + $url = 'https://123456.ngrok.io?extra%5B%5D=1&extra%5B%5D=2&shop=some-shop.myshopify.com&path_prefix=%2Fapps%2Fawesome_reviews×tamp=1337178173'; + $params = Utils::getQueryParams($url); + $secret = 'test-secret'; + $sortedParams = 'extra=1,2path_prefix=/apps/awesome_reviewsshop=some-shop.myshopify.comtimestamp=1337178173'; + $params['signature'] = hash_hmac('sha256', $sortedParams, $secret); + + // Check with a wrong secret + $this->assertEquals(false, Utils::validateHmac( + $params, + $secret . 'wrong' + )); + + // Check with the correct secret but with an altered request + $params['foo'] = 'bar'; + $this->assertEquals(false, Utils::validateHmac( + $params, + $secret + )); + } + public function testGetValidQueryParams() { $params = [