From cfcaa02e591647914c2eedb46e0c9c254bf7d0e1 Mon Sep 17 00:00:00 2001 From: Kohei Matsumoto Date: Wed, 13 Jul 2022 10:32:14 +0900 Subject: [PATCH 1/2] feat: Add signature validation --- src/Utils.php | 25 +++++++++++++++++++++++++ tests/UtilsTest.php | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/Utils.php b/src/Utils.php index 42300426..2ff245e8 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -64,6 +64,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 f271b34c..5aa1efe4 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -94,6 +94,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 = [ From 8c69047cbb7345b54c1befd3ccc04f450d2dbab6 Mon Sep 17 00:00:00 2001 From: Kohei Matsumoto Date: Wed, 13 Jul 2022 10:38:34 +0900 Subject: [PATCH 2/2] docs: Add validateSignature to utils --- docs/usage/utils.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/usage/utils.md b/docs/usage/utils.md index c142b709..c7666efa 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.