From ce38b05eae238551b21ac6c58f2fcb2931386e50 Mon Sep 17 00:00:00 2001 From: "khanh.nguyen" Date: Mon, 16 Aug 2021 16:33:34 +0200 Subject: [PATCH] Update request validator to support new webhook signature --- composer.json | 3 +- examples/signed-request-validation.php | 26 ++ examples/signedrequest-verification.php | 19 - src/MessageBird/Objects/SignedRequest.php | 4 + src/MessageBird/RequestValidator.php | 102 ++++- tests/Unit/Data/webhooksignature.json | 442 ++++++++++++++++++++++ tests/Unit/RequestValidatorTest.php | 101 +++++ tests/Unit/SignedRequestTest.php | 3 + 8 files changed, 676 insertions(+), 24 deletions(-) create mode 100644 examples/signed-request-validation.php delete mode 100644 examples/signedrequest-verification.php create mode 100644 tests/Unit/Data/webhooksignature.json diff --git a/composer.json b/composer.json index 21c16b93..8daca80e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "require": { "php": ">=7.3|^8.0", "ext-curl": "*", - "ext-json": "*" + "ext-json": "*", + "firebase/php-jwt": "^5.4" }, "require-dev": { "phpunit/phpunit": "^8.0|^9.0", diff --git a/examples/signed-request-validation.php b/examples/signed-request-validation.php new file mode 100644 index 00000000..397c5132 --- /dev/null +++ b/examples/signed-request-validation.php @@ -0,0 +1,26 @@ +validateRequestFromGlobals(); +} catch (\MessageBird\Exceptions\ValidationException $e) { + // The request was invalid, so respond accordingly. + http_response_code(412); +} + +// Or directly verify the signature of the incoming request +$signature = 'JWT_TOKEN_STRING'; +$url = 'https://yourdomain.com/path'; +$body = 'REQUEST_BODY'; + +try { + $request = $requestValidator->validateSignature($signature, $url, $body); +} catch (\MessageBird\Exceptions\ValidationException $e) { + // The request was invalid, so respond accordingly. + http_response_code(412); +} diff --git a/examples/signedrequest-verification.php b/examples/signedrequest-verification.php deleted file mode 100644 index 11f5ca36..00000000 --- a/examples/signedrequest-verification.php +++ /dev/null @@ -1,19 +0,0 @@ -verify($request)) { - // The request was invalid, so respond accordingly. - http_response_code(412); -} diff --git a/src/MessageBird/Objects/SignedRequest.php b/src/MessageBird/Objects/SignedRequest.php index 5c16ac29..3d61fa8d 100644 --- a/src/MessageBird/Objects/SignedRequest.php +++ b/src/MessageBird/Objects/SignedRequest.php @@ -3,6 +3,7 @@ namespace MessageBird\Objects; use MessageBird\Exceptions\ValidationException; +use MessageBird\RequestValidator; /** * Class SignedRequest @@ -10,6 +11,7 @@ * @package MessageBird\Objects * * @link https://developers.messagebird.com/docs/verify-http-requests + * @deprecated Use {@link RequestValidator} instead. */ class SignedRequest extends Base { @@ -46,6 +48,7 @@ class SignedRequest extends Base * * @return SignedRequest * @throws ValidationException when a required parameter is missing. + * @deprecated Use {@link RequestValidator::validateRequestFromGlobals()} instead. */ public static function createFromGlobals() { @@ -70,6 +73,7 @@ public static function createFromGlobals() * @param string $body The request body * @return SignedRequest * @throws ValidationException when a required parameter is missing. + * @deprecated Use {@link RequestValidator::validateSignature()} instead. */ public static function create($query, $signature, $requestTimestamp, $body) { diff --git a/src/MessageBird/RequestValidator.php b/src/MessageBird/RequestValidator.php index f8cc2caf..c2638f88 100644 --- a/src/MessageBird/RequestValidator.php +++ b/src/MessageBird/RequestValidator.php @@ -2,17 +2,22 @@ namespace MessageBird; +use Firebase\JWT\JWT; +use Firebase\JWT\SignatureInvalidException; +use MessageBird\Exceptions\ValidationException; use MessageBird\Objects\SignedRequest; /** - * Class RequestValidator + * Class RequestValidator validates request signature signed by MessageBird services. * * @package MessageBird + * @see https://developers.messagebird.com/docs/verify-http-requests */ class RequestValidator { const BODY_HASH_ALGO = 'sha256'; const HMAC_HASH_ALGO = 'sha256'; + const ALLOWED_ALGOS = array('HS256', 'HS384', 'HS512'); /** * The key with which requests will be signed by MessageBird. @@ -21,14 +26,28 @@ class RequestValidator */ private $signingKey; + /** + * This field instructs Validator to not validate url_hash claim. + * It is recommended to not skip URL validation to ensure high security. + * but the ability to skip URL validation is necessary in some cases, e.g. + * your service is behind proxy or when you want to validate it yourself. + * Note that when true, no query parameters should be trusted. + * Defaults to false. + * + * @var bool + */ + private $skipURLValidation; + /** * RequestValidator constructor. * - * @param string $signingKey + * @param string $signingKey customer signature key. Can be retrieved through Developer Settings. This is NOT your API key. + * @param bool $skipURLValidation whether url_hash claim validation should be skipped. Note that when true, no query parameters should be trusted. */ - public function __construct($signingKey) + public function __construct(string $signingKey, bool $skipURLValidation = false) { $this->signingKey = $signingKey; + $this->skipURLValidation = $skipURLValidation; } /** @@ -36,6 +55,7 @@ public function __construct($signingKey) * * @param SignedRequest $request * @return bool + * @deprecated Use {@link RequestValidator::validateSignature()} instead. */ public function verify(SignedRequest $request) { @@ -47,6 +67,9 @@ public function verify(SignedRequest $request) return \hash_equals($expectedSignature, $calculatedSignature); } + /** + * @deprecated Use {@link RequestValidator::validateSignature()} instead. + */ private function buildPayloadFromRequest(SignedRequest $request): string { $parts = []; @@ -71,9 +94,80 @@ private function buildPayloadFromRequest(SignedRequest $request): string * @param SignedRequest $request The signed request object. * @param int $offset The maximum number of seconds that is allowed to consider the request recent * @return bool + * @deprecated Use {@link RequestValidator::validateSignature()} instead. */ public function isRecent(SignedRequest $request, $offset = 10) { - return (\time() - (int) $request->requestTimestamp) < $offset; + return (\time() - (int)$request->requestTimestamp) < $offset; + } + + /** + * Validate JWT signature. + * This JWT is signed with a MessageBird account unique secret key, ensuring the request is from MessageBird and a specific account. + * The JWT contains the following claims: + * - "url_hash" - the raw URL hashed with SHA256 ensuring the URL wasn't altered. + * - "payload_hash" - the raw payload hashed with SHA256 ensuring the payload wasn't altered. + * - "jti" - a unique token ID to implement an optional non-replay check (NOT validated by default). + * - "nbf" - the not before timestamp. + * - "exp" - the expiration timestamp is ensuring that a request isn't captured and used at a later time. + * - "iss" - the issuer name, always MessageBird. + * + * @param string $signature the actual signature taken from request header "MessageBird-Signature-JWT". + * @param string $url the raw url including the protocol, hostname and query string, {@code https://example.com/?example=42}. + * @param string $body the raw request body. + * @return object JWT token payload + * @throws ValidationException if signature validation fails. + * + * @see https://developers.messagebird.com/docs/verify-http-requests + */ + public function validateSignature(string $signature, string $url, string $body) + { + if (empty($signature)) { + throw new ValidationException("Signature cannot be empty."); + } + if (!$this->skipURLValidation && empty($url)) { + throw new ValidationException("URL cannot be empty"); + } + + JWT::$leeway = 1; + try { + $decoded = JWT::decode($signature, $this->signingKey, self::ALLOWED_ALGOS); + } catch (\InvalidArgumentException | \UnexpectedValueException | SignatureInvalidException $e) { + throw new ValidationException($e->getMessage(), $e->getCode(), $e); + } + + if (empty($decoded->iss) || $decoded->iss !== 'MessageBird') { + throw new ValidationException('invalid jwt: claim iss has wrong value'); + } + + if (!$this->skipURLValidation && !hash_equals(hash(self::HMAC_HASH_ALGO, $url), $decoded->url_hash)) { + throw new ValidationException('invalid jwt: claim url_hash is invalid'); + } + + switch (true) { + case empty($body) && !empty($decoded->payload_hash): + throw new ValidationException('invalid jwt: claim payload_hash is set but actual payload is missing'); + case !empty($body) && empty($decoded->payload_hash): + throw new ValidationException('invalid jwt: claim payload_hash is not set but payload is present'); + case !empty($body) && !hash_equals(hash(self::HMAC_HASH_ALGO, $body), $decoded->payload_hash): + throw new ValidationException('invalid jwt: claim payload_hash is invalid'); + } + + return $decoded; + } + + /** + * Validate request signature from PHP globals. + * + * @return object JWT token payload + * @throws ValidationException if signature validation fails. + */ + public function validateRequestFromGlobals() + { + $signature = $_SERVER['MessageBird-Signature-JWT'] ?? null; + $url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; + $body = file_get_contents('php://input'); + + return $this->validateSignature($signature, $url, $body); } } diff --git a/tests/Unit/Data/webhooksignature.json b/tests/Unit/Data/webhooksignature.json new file mode 100644 index 00000000..c870f15b --- /dev/null +++ b/tests/Unit/Data/webhooksignature.json @@ -0,0 +1,442 @@ +[ + { + "name": "Valid JWT with no URL parameters or payload - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": true + }, + { + "name": "Valid JWT with no URL parameters or payload - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": true + }, + { + "name": "Valid JWT with no URL parameters or payload - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": true + }, + { + "name": "Valid JWT with no URL parameters or payload - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": true + }, + { + "name": "Valid JWT with no URL parameters or payload - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and no payload - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiJjOTQ2YWY3Ny1lMTgyLTRlYWEtYjJmZi0xYTU0NWI1ZTk5MWEiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAifQ.huo2ou6JDoDc7sV25d75UMWeYhBeavQlLsqIibSZuac", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and no payload - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiJjOTQ2YWY3Ny1lMTgyLTRlYWEtYjJmZi0xYTU0NWI1ZTk5MWEiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAifQ.huo2ou6JDoDc7sV25d75UMWeYhBeavQlLsqIibSZuac", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and no payload - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiJjOTQ2YWY3Ny1lMTgyLTRlYWEtYjJmZi0xYTU0NWI1ZTk5MWEiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAifQ.huo2ou6JDoDc7sV25d75UMWeYhBeavQlLsqIibSZuac", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and no payload - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiJjOTQ2YWY3Ny1lMTgyLTRlYWEtYjJmZi0xYTU0NWI1ZTk5MWEiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAifQ.huo2ou6JDoDc7sV25d75UMWeYhBeavQlLsqIibSZuac", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and no payload - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiJjOTQ2YWY3Ny1lMTgyLTRlYWEtYjJmZi0xYTU0NWI1ZTk5MWEiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAifQ.huo2ou6JDoDc7sV25d75UMWeYhBeavQlLsqIibSZuac", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and payload - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI5M2U1NTAwNi1hMGU4LTQ1MjYtYTE5MC1mYTVmZjAwZWExMTYiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAiLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0._H--TOuYFLpeEH39-rg5E3IHVkjHozBcaKVWPRC5m9I", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and payload - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI5M2U1NTAwNi1hMGU4LTQ1MjYtYTE5MC1mYTVmZjAwZWExMTYiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAiLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0._H--TOuYFLpeEH39-rg5E3IHVkjHozBcaKVWPRC5m9I", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and payload - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI5M2U1NTAwNi1hMGU4LTQ1MjYtYTE5MC1mYTVmZjAwZWExMTYiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAiLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0._H--TOuYFLpeEH39-rg5E3IHVkjHozBcaKVWPRC5m9I", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and payload - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI5M2U1NTAwNi1hMGU4LTQ1MjYtYTE5MC1mYTVmZjAwZWExMTYiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAiLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0._H--TOuYFLpeEH39-rg5E3IHVkjHozBcaKVWPRC5m9I", + "valid": true + }, + { + "name": "Valid JWT with URL parameters and payload - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?bar=1\u0026foo=2", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI5M2U1NTAwNi1hMGU4LTQ1MjYtYTE5MC1mYTVmZjAwZWExMTYiLCJ1cmxfaGFzaCI6IjQxZjA1ZjBkZGQwYTIyYWIyMDlhYzQ2ZjQ3YzQ1NzJkOWNlZmEyNTdlZDc0YjI0MDA0YmFlNzUzZWNlNmMyNjAiLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0._H--TOuYFLpeEH39-rg5E3IHVkjHozBcaKVWPRC5m9I", + "valid": true + }, + { + "name": "Token received before it was issued - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ4MjgwMCwiZXhwIjoxNjI1NDgyODYwLCJqdGkiOiJmOWY4YzM4Mi0yNDQ5LTQzMTEtYjcyYi0xZGY3MTY4NzkzMWUiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.ZELgDFNGhjZH9CffQKcq3sytBe2I0KciLxpBhcfstHQ", + "valid": false, + "reason": "invalid jwt: claim nbf is in the future" + }, + { + "name": "Token received after it was expired - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3NTYwMCwiZXhwIjoxNjI1NDc1NjYwLCJqdGkiOiI1ZjAyZjUyMi02MDMwLTQ2YzgtYjVhMy0wMTI0NjQ3OGQ4YmMiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.45MSST3B_2PsjNUeiuW54_vUQgVw4rBXrdWrOUEz3lM", + "valid": false, + "reason": "invalid jwt: claim exp is in the past" + }, + { + "name": "Token received on different URL (parameters were sorted out of order) - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/path?foo=1\u0026bar=2", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiJhNzVjOTA5Ni1lODIzLTQ0MmItODVmMi03ZDNjOWQ5YjcyNmIiLCJ1cmxfaGFzaCI6IjZhMDIyZDgzNGMxODMzMGJhYWQ4YTJiZDYxMzUxNzNlZDIxNjEwYTQ1NDUxNmJlMGRmM2YxY2MwMTUxNjEwZWEifQ.z6Sw1XQIM0wuEQGBhXBdawDIIrtMg2XnmA_bpDq53pE", + "valid": false, + "reason": "invalid jwt: claim url_hash is invalid" + }, + { + "name": "Payload does not match - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJmYjYyZjAyYWNkYTdkNzQxNzdhNzAxYTFjZTAwNmU2YmFjZDkwYzdkNGQ3YWI0ODE2OTJjMWRhNDdjODEwNzZiIn0.m79RCwO6dGa9pvzsQypkVuXQ6eM0CkRl6_U7MfWg6TM", + "valid": false, + "reason": "invalid jwt: claim payload_hash is invalid" + }, + { + "name": "Payload does not match - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJmYjYyZjAyYWNkYTdkNzQxNzdhNzAxYTFjZTAwNmU2YmFjZDkwYzdkNGQ3YWI0ODE2OTJjMWRhNDdjODEwNzZiIn0.m79RCwO6dGa9pvzsQypkVuXQ6eM0CkRl6_U7MfWg6TM", + "valid": false, + "reason": "invalid jwt: claim payload_hash is invalid" + }, + { + "name": "Payload does not match - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJmYjYyZjAyYWNkYTdkNzQxNzdhNzAxYTFjZTAwNmU2YmFjZDkwYzdkNGQ3YWI0ODE2OTJjMWRhNDdjODEwNzZiIn0.m79RCwO6dGa9pvzsQypkVuXQ6eM0CkRl6_U7MfWg6TM", + "valid": false, + "reason": "invalid jwt: claim payload_hash is invalid" + }, + { + "name": "Payload does not match - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJmYjYyZjAyYWNkYTdkNzQxNzdhNzAxYTFjZTAwNmU2YmFjZDkwYzdkNGQ3YWI0ODE2OTJjMWRhNDdjODEwNzZiIn0.m79RCwO6dGa9pvzsQypkVuXQ6eM0CkRl6_U7MfWg6TM", + "valid": false, + "reason": "invalid jwt: claim payload_hash is invalid" + }, + { + "name": "Payload does not match - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJmYjYyZjAyYWNkYTdkNzQxNzdhNzAxYTFjZTAwNmU2YmFjZDkwYzdkNGQ3YWI0ODE2OTJjMWRhNDdjODEwNzZiIn0.m79RCwO6dGa9pvzsQypkVuXQ6eM0CkRl6_U7MfWg6TM", + "valid": false, + "reason": "invalid jwt: claim payload_hash is invalid" + }, + { + "name": "Different secret - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIyNDNjMjdhZS0yZjAyLTQ2YTAtODg1Mi1jNjZmMzdlYTlmNDYiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.JgSeYyOtlEKAXk8bz30iJI4tXgf4lxknoiezawuVhb4", + "valid": false, + "reason": "invalid jwt: signature is invalid" + }, + { + "name": "payload was removed in transit - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0.bHfsjj3tljfzTufoG3EU3p_yFrQh9CufyzRSaRMd4ss", + "valid": false, + "reason": "invalid jwt: claim payload_hash is set but actual payload is missing" + }, + { + "name": "payload was removed in transit - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0.bHfsjj3tljfzTufoG3EU3p_yFrQh9CufyzRSaRMd4ss", + "valid": false, + "reason": "invalid jwt: claim payload_hash is set but actual payload is missing" + }, + { + "name": "payload was removed in transit - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0.bHfsjj3tljfzTufoG3EU3p_yFrQh9CufyzRSaRMd4ss", + "valid": false, + "reason": "invalid jwt: claim payload_hash is set but actual payload is missing" + }, + { + "name": "payload was removed in transit - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0.bHfsjj3tljfzTufoG3EU3p_yFrQh9CufyzRSaRMd4ss", + "valid": false, + "reason": "invalid jwt: claim payload_hash is set but actual payload is missing" + }, + { + "name": "payload was removed in transit - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiIxNDUwMTUzMi05NmYyLTQ2ODQtOTgzMi02OGYwOTUxYWUzNDIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiJkZmZkNjAyMWJiMmJkNWIwYWY2NzYyOTA4MDllYzNhNTMxOTFkZDgxYzdmNzBhNGIyODY4OGEzNjIxODI5ODZmIn0.bHfsjj3tljfzTufoG3EU3p_yFrQh9CufyzRSaRMd4ss", + "valid": false, + "reason": "invalid jwt: claim payload_hash is set but actual payload is missing" + }, + { + "name": "payload was added in transit - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": false, + "reason": "invalid jwt: claim payload_hash is not set but payload is present" + }, + { + "name": "payload was added in transit - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": false, + "reason": "invalid jwt: claim payload_hash is not set but payload is present" + }, + { + "name": "payload was added in transit - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": false, + "reason": "invalid jwt: claim payload_hash is not set but payload is present" + }, + { + "name": "payload was added in transit - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": false, + "reason": "invalid jwt: claim payload_hash is not set but payload is present" + }, + { + "name": "payload was added in transit - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Hello, World!", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.6Fp0rVOsRr1fj9S2GidXAfkmPVofKr8_RTffC6G6r2E", + "valid": false, + "reason": "invalid jwt: claim payload_hash is not set but payload is present" + }, + { + "name": "Port number in URL - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com:8080/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjA3Zjc4NjFhNjliMTNjMjI5YzZhZTU5NmQxOThiMjc2M2IwNTRjZjVlMDAwZDczNmQxYzQ4MTAyZDc3MGNmZmMifQ.vcpa0IicGAbpo064dOjiA2VbVOR2S5uZBEaBOP16sS8", + "valid": true + }, + { + "name": "Special characters in URL - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/?obj%5Bfield1%5D=val1\u0026obj%5Bfield2%5D=val2\u0026param=value+with+spaces", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjhhZmYzNzQ4ZDc4OWM0MzMzNTczZTFjMzFmZTViMDA0OTQwY2I1ZmM0NzQ2MGVkM2QwZTY1NGY3ZmM0ZTVmYWUifQ.mJBCMfEjKmN3IMuzqPXPHktWETrcu0iNdF3agE8PDyI", + "valid": true + }, + { + "name": "Special characters in the payload - DELETE", + "method": "DELETE", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Some text containg \u0026 \u003c \u003e, \u0026 and \\u0026", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiIyNDMxYmQ5Y2NiNDhlZDAxNmZhZDU2N2IwNzcxMjAzOGI1Y2RkOTQ5YWM5ZDQxZmU0NjkzZDVhMzg0NWNmN2U4In0.IQiKzeEPaO3A48lhKmBDSmxZyCESPCGxdSKFdi0dEJs", + "valid": true + }, + { + "name": "Special characters in the payload - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Some text containg \u0026 \u003c \u003e, \u0026 and \\u0026", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiIyNDMxYmQ5Y2NiNDhlZDAxNmZhZDU2N2IwNzcxMjAzOGI1Y2RkOTQ5YWM5ZDQxZmU0NjkzZDVhMzg0NWNmN2U4In0.IQiKzeEPaO3A48lhKmBDSmxZyCESPCGxdSKFdi0dEJs", + "valid": true + }, + { + "name": "Special characters in the payload - PATCH", + "method": "PATCH", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Some text containg \u0026 \u003c \u003e, \u0026 and \\u0026", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiIyNDMxYmQ5Y2NiNDhlZDAxNmZhZDU2N2IwNzcxMjAzOGI1Y2RkOTQ5YWM5ZDQxZmU0NjkzZDVhMzg0NWNmN2U4In0.IQiKzeEPaO3A48lhKmBDSmxZyCESPCGxdSKFdi0dEJs", + "valid": true + }, + { + "name": "Special characters in the payload - POST", + "method": "POST", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Some text containg \u0026 \u003c \u003e, \u0026 and \\u0026", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiIyNDMxYmQ5Y2NiNDhlZDAxNmZhZDU2N2IwNzcxMjAzOGI1Y2RkOTQ5YWM5ZDQxZmU0NjkzZDVhMzg0NWNmN2U4In0.IQiKzeEPaO3A48lhKmBDSmxZyCESPCGxdSKFdi0dEJs", + "valid": true + }, + { + "name": "Special characters in the payload - PUT", + "method": "PUT", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "payload": "Some text containg \u0026 \u003c \u003e, \u0026 and \\u0026", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDciLCJwYXlsb2FkX2hhc2giOiIyNDMxYmQ5Y2NiNDhlZDAxNmZhZDU2N2IwNzcxMjAzOGI1Y2RkOTQ5YWM5ZDQxZmU0NjkzZDVhMzg0NWNmN2U4In0.IQiKzeEPaO3A48lhKmBDSmxZyCESPCGxdSKFdi0dEJs", + "valid": true + }, + { + "name": "HS384 alg - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.KNMW-29X77lZeuPmThmHWc_RUAvTkaDkpxIZK6mqE08v8mWKiU9Edh4QXwAJO2nv", + "valid": true + }, + { + "name": "HS512 alg - GET", + "method": "GET", + "secret": "36efdd1aace2e26cd490f0d951138253bef2f7c6d34d18981da781555cc4cebb", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJNZXNzYWdlQmlyZCIsIm5iZiI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.rd6r0iMyNGnVPCwurETphE3Y8rpAyvvnUK0S8WvGUkt2E1QSRAZ7NZJZBHw1Y_Wb5W-sK9HJr_PRL2vz4jRT3Q", + "valid": true + }, + { + "name": "none alg - GET", + "method": "GET", + "url": "https://example.com/", + "timestamp": "2021-07-05T12:00:00+02:00", + "token": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJNZXNzYWdlQmlyZCIsImlhdCI6MTYyNTQ3OTIwMCwiZXhwIjoxNjI1NDc5MjYwLCJqdGkiOiI1OWEyNDRkYy1lOWFkLTRlMjMtOTc3OC0zNzFmYWEyMzhmNzIiLCJ1cmxfaGFzaCI6IjBmMTE1ZGIwNjJiN2MwZGQwMzBiMTY4NzhjOTlkZWE1YzM1NGI0OWRjMzdiMzhlYjg4NDYxNzljNzc4M2U5ZDcifQ.", + "valid": false, + "reason": "invalid jwt: signing method none is invalid" + } +] diff --git a/tests/Unit/RequestValidatorTest.php b/tests/Unit/RequestValidatorTest.php index 5a0e7fe4..39768382 100644 --- a/tests/Unit/RequestValidatorTest.php +++ b/tests/Unit/RequestValidatorTest.php @@ -2,12 +2,18 @@ namespace Tests\Unit; +use DateTime; +use Firebase\JWT\JWT; +use InvalidArgumentException; +use Iterator; +use MessageBird\Exceptions\ValidationException; use MessageBird\Objects\SignedRequest; use MessageBird\RequestValidator; use PHPUnit\Framework\TestCase; class RequestValidatorTest extends TestCase { + /* @deprecated */ public function testVerify() { $query = [ @@ -29,6 +35,7 @@ public function testVerify() $this->assertTrue($validator->verify($request)); } + /* @deprecated */ public function testVerifyWithBody() { $query = [ @@ -50,6 +57,7 @@ public function testVerifyWithBody() $this->assertTrue($validator->verify($request)); } + /* @deprecated */ public function testVerificationFails() { $query = [ @@ -71,6 +79,7 @@ public function testVerificationFails() $this->assertFalse($validator->verify($request)); } + /* @deprecated */ public function testRecentRequest() { $query = []; @@ -84,6 +93,7 @@ public function testRecentRequest() $this->assertTrue($validator->isRecent($request)); } + /* @deprecated */ public function testExpiredRequest() { $query = []; @@ -96,4 +106,95 @@ public function testExpiredRequest() $this->assertFalse($validator->isRecent($request)); } + + const ERROR_MAP = [ + "invalid jwt: signing method none is invalid" => "Algorithm not supported", + "invalid jwt: claim nbf is in the future" => "Cannot handle token prior to", + "invalid jwt: claim exp is in the past" => "Expired token", + "invalid jwt: signature is invalid" => "Signature verification failed" + ]; + + /** + * Load From Array + * + * @dataProvider loadFromArrayDataProvider() + */ + public function testLoadFromArray($testCase): void + { + if (!$testCase['valid']) { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage(self::ERROR_MAP[$testCase['reason']] ?? $testCase['reason']); + } + + $requestValidator = new RequestValidator($testCase['secret'] ?? 'random-secret-to-not-fail-test'); + + // Reset JWT timestamp to test case timestamp. + $d = new DateTime($testCase['timestamp']); + JWT::$timestamp = $d->getTimestamp(); + + $decoded = $requestValidator->validateSignature($testCase['token'], $testCase['url'], $testCase['payload'] ?? ''); + + $this->assertNotEmpty($decoded); + } + + public function loadFromArrayDataProvider() + { + $file = dirname(__FILE__) . '/Data/webhooksignature.json'; + if (!is_file($file)) { + throw new InvalidArgumentException( + 'Could not find test data file from path: "' . $file . '".' + ); + } + + $jsonData = json_decode( + file_get_contents($file), + true + ); + if ($jsonData === null) { + throw new InvalidArgumentException( + 'Invalid json: "' . json_last_error_msg() . '".', + 1451045747 + ); + } + return new WebhookSignatureTestIterator($jsonData); + } +} + +/** + * Class WebhookSignatureTestIterator + * @package Tests\Unit + */ +class WebhookSignatureTestIterator implements Iterator +{ + protected $array = []; + + public function __construct($array) + { + $this->array = $array; + } + + function rewind() + { + return reset($this->array); + } + + function current() + { + return [current($this->array)]; + } + + function key() + { + return current($this->array)['name']; + } + + function next() + { + return [next($this->array)]; + } + + function valid() + { + return key($this->array) !== null; + } } diff --git a/tests/Unit/SignedRequestTest.php b/tests/Unit/SignedRequestTest.php index bdef3e96..400d6eb0 100644 --- a/tests/Unit/SignedRequestTest.php +++ b/tests/Unit/SignedRequestTest.php @@ -5,6 +5,9 @@ use MessageBird\Objects\SignedRequest; use PHPUnit\Framework\TestCase; +/** + * @deprecated This test is deprecated together with {@link SignedRequest} + */ class SignedRequestTest extends TestCase { public function testCreate()