From 57eb430da54de71b9fb914e98f9ebd171b167f5a Mon Sep 17 00:00:00 2001 From: Firdaus Zahari Date: Thu, 21 Mar 2024 21:56:50 +0800 Subject: [PATCH 1/3] Add additional verification method to the Postmark provider --- README.md | 17 ++++ config/receiver.php | 41 +++++++++ src/Providers/PostmarkProvider.php | 34 +++++-- src/ReceiverServiceProvider.php | 4 + tests/PostmarkProviderTest.php | 138 +++++++++++++++++++++++++++++ tests/TestCase.php | 10 +++ 6 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 config/receiver.php create mode 100644 tests/PostmarkProviderTest.php diff --git a/README.md b/README.md index dc53229..42de0e5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Of course, Receiver can receive webhooks from any source using [custom providers ## Table of Contents - [Installation](#installation) +- [Configuration](#configuration) - [Receiving Webhooks](#receiving-webhooks) - [The Basics](#the-basics) - [Receiving from multiple apps](#receiving-from-multiple-apps) @@ -53,6 +54,22 @@ Optional: **Stripe** support requires [`stripe/stripe-php`](https://github.com/stripe/stripe-php) +## Configuration + +Currently, configuration is only supported for the Postmark provider. To publish the configuration to your Laravel app, run this command: + +```shell +php artisan vendor:publish --provider="Receiver\ReceiverServiceProvider" +``` + +A new config called `receiver.php` will be created in the `config` directory. + +For Postmark provider, there are three different verification types are available. + +1. `auth` - Perform verification via the HTTP authentication. +2. `headers` - Perform verification based on the predefined key-value pair of expected headers in the config. Additional headers for the webhook can be configured directly in the Postmark dashboard. +3. `ips` - Perform verification based on the IP addresses in the allow-list. By default, [these IP addresses](https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks) are added to the allow-list. + ## Receiving Webhooks ### The Basics diff --git a/config/receiver.php b/config/receiver.php new file mode 100644 index 0000000..c34821e --- /dev/null +++ b/config/receiver.php @@ -0,0 +1,41 @@ + [ + /** + * Set the verification types to be used to verify Postmark webhook. + * The order of the verification types determines which one will be run first. + * All verification types need to pass in order for a webhook request to be verified. + * + * Supported types: "auth", "headers", "ips" + */ + 'verification_types' => [ + 'auth', + 'headers', + 'ips', + ], + + /** + * Set the combination of key-value pairs of headers to be verified against + * the webhook request. Currently, Postmark will send the "User-Agent" header with + * value of "Postmark" by default. + * + * Additional headers can be configured in the Postmark webhook management page. + */ + 'headers' => [ + 'User-Agent' => 'Postmark', + ], + + /** + * Set a list of IPs that is allowed to make the webhook request. + * By default, this options is populated with IPs provided by Postmark + * on https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks + */ + 'ips' => [ + '3.134.147.250', + '50.31.156.6', + '50.31.156.77', + '18.217.206.57', + ], + ], +]; diff --git a/src/Providers/PostmarkProvider.php b/src/Providers/PostmarkProvider.php index 825e32f..a3de4ba 100644 --- a/src/Providers/PostmarkProvider.php +++ b/src/Providers/PostmarkProvider.php @@ -15,13 +15,37 @@ class PostmarkProvider extends AbstractProvider */ public function verify(Request $request): bool { - try { - Auth::onceBasic(); + foreach (config('receiver.postmark.verification_types') as $verification_type) { + switch ($verification_type) { + case 'auth': + try { + Auth::onceBasic(); + } catch (\Exception $exception) { + return false; + } + break; - return true; - } catch (\Exception $exception) { - return false; + case 'headers': + foreach (config('receiver.postmark.headers') as $key => $value) { + if (!$request->hasHeader($key)) { + return false; + } + + if ($request->header($key) !== $value) { + return false; + } + } + break; + + case 'ips': + if (!in_array($request->getClientIp(), config('receiver.postmark.ips'), true)) { + return false; + } + break; + } } + + return true; } /** diff --git a/src/ReceiverServiceProvider.php b/src/ReceiverServiceProvider.php index d709a72..5768c8b 100644 --- a/src/ReceiverServiceProvider.php +++ b/src/ReceiverServiceProvider.php @@ -30,6 +30,10 @@ public function boot() GenerateReceiver::class, ]); } + + $this->publishes([ + __DIR__.'/../config/receiver.php' => config_path('receiver.php'), + ]); } /** diff --git a/tests/PostmarkProviderTest.php b/tests/PostmarkProviderTest.php new file mode 100644 index 0000000..1419953 --- /dev/null +++ b/tests/PostmarkProviderTest.php @@ -0,0 +1,138 @@ +setupBaseRequest($request); + + $provider = new PostmarkProvider; + $provider->receive($request); + + $webhook = $provider->webhook(); + + $this->assertInstanceOf(Webhook::class, $webhook); + } + + public function test_it_will_verify_postmark_webhook_with_valid_headers() + { + Config::set('receiver.postmark.verification_types', ['headers']); + + $request = Mockery::mock(Request::class); + + $this->setupBaseRequest($request); + + $request->allows('hasHeader')->with('foo')->andReturns(true); + $request->allows('header')->with('foo')->andReturns('bar'); + + $provider = new PostmarkProvider; + $provider->receive($request); + + $webhook = $provider->webhook(); + + $this->assertInstanceOf(Webhook::class, $webhook); + } + + public function test_it_will_deny_postmark_webhook_with_missing_headers() + { + Config::set('receiver.postmark.verification_types', ['headers']); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Unauthorized'); + + $request = Mockery::mock(Request::class); + + $this->setupBaseRequest($request); + + $request->allows('hasHeader')->with('foo')->andReturns(false); + + $provider = new PostmarkProvider; + $provider->receive($request); + } + + public function test_it_will_verify_postmark_webhook_with_valid_ip() + { + Config::set('receiver.postmark.verification_types', ['ips']); + + $request = Mockery::mock(Request::class); + + $this->setupBaseRequest($request); + + $request->allows('getClientIp')->andReturns('123.123.123.123'); + + $provider = new PostmarkProvider; + $provider->receive($request); + + $webhook = $provider->webhook(); + + $this->assertInstanceOf(Webhook::class, $webhook); + } + + public function test_it_will_deny_postmark_webhook_with_invalid_ip() + { + Config::set('receiver.postmark.verification_types', ['ips']); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Unauthorized'); + + $request = Mockery::mock(Request::class); + + $this->setupBaseRequest($request); + + $request->allows('getClientIp')->andReturns('111.111.111.111'); + + $provider = new PostmarkProvider; + $provider->receive($request); + } + + protected function setupBaseRequest(Request $request): Request + { + $request->allows('filled')->with('RecordType')->andReturns($this->mockPayload('RecordType')); + $request->allows('input')->with('RecordType')->andReturns($this->mockPayload('RecordType')); + $request->allows('all')->andReturns($this->mockPayload()); + $request->allows('header')->with('User-Agent')->andReturns('Postmark'); + $request->allows('hasHeader')->with('User-Agent')->andReturns(true); + $request->allows('getContent')->andReturns(json_encode($this->mockPayload())); + + return $request; + } + + /** + * https://postmarkapp.com/developer/webhooks/delivery-webhook#testing-with-curl + * + * @param string|null $key + * @return mixed + */ + protected function mockPayload(string $key = null): mixed + { + $data = [ + 'MessageID' => '883953f4-6105-42a2-a16a-77a8eac79483', + 'Recipient' => 'john@example.com', + 'DeliveredAt' => '2014-08-01T13:28:10.2735393-04:00', + 'Details' => 'Test delivery webhook details', + 'Tag' => 'welcome-email', + 'ServerID' => 23, + 'Metadata' => [ + 'a_key' => 'a_value', + 'b_key' => 'b_value' + ], + 'RecordType' => 'Delivery', + 'MessageStream' => 'outbound', + ]; + + return $key ? data_get($data, $key) : $data; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 67036d2..6996659 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,6 +26,16 @@ protected function getEnvironmentSetUp($app) 'redirect' => 'http://your-callback-url', 'webhook_secret' => 'slack-webhook-secret', ]); + + $app['config']->set('receiver.postmark', [ + 'headers' => [ + 'User-Agent' => 'Postmark', + 'foo' => 'bar', + ], + 'ips' => [ + '123.123.123.123', + ], + ]); } /** From cfd53e1bcfa5cdc449cd9e93e47b44f16a84accc Mon Sep 17 00:00:00 2001 From: Firdaus Zahari Date: Thu, 21 Mar 2024 22:10:23 +0800 Subject: [PATCH 2/3] Add test for invalid header --- tests/PostmarkProviderTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/PostmarkProviderTest.php b/tests/PostmarkProviderTest.php index 1419953..a828f95 100644 --- a/tests/PostmarkProviderTest.php +++ b/tests/PostmarkProviderTest.php @@ -63,6 +63,26 @@ public function test_it_will_deny_postmark_webhook_with_missing_headers() $provider->receive($request); } + public function test_it_will_deny_postmark_webhook_with_invalid_headers() + { + Config::set('receiver.postmark.verification_types', ['headers']); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Unauthorized'); + + $request = Mockery::mock(Request::class); + + $this->setupBaseRequest($request); + + $request->allows('hasHeader')->with('foo')->andReturns(true); + $request->allows('header')->with('foo')->andReturns('baz'); + + $request->allows('hasHeader')->with('foo')->andReturns(false); + + $provider = new PostmarkProvider; + $provider->receive($request); + } + public function test_it_will_verify_postmark_webhook_with_valid_ip() { Config::set('receiver.postmark.verification_types', ['ips']); From 288893036f854d748c9e46d0a5ba7b2cb8254a83 Mon Sep 17 00:00:00 2001 From: Firdaus Zahari Date: Sun, 5 May 2024 21:26:25 +0800 Subject: [PATCH 3/3] Remove custom config file --- README.md | 32 ++++++++++++++++++----- config/receiver.php | 41 ------------------------------ src/Providers/PostmarkProvider.php | 6 ++--- src/ReceiverServiceProvider.php | 4 --- tests/PostmarkProviderTest.php | 12 ++++----- tests/TestCase.php | 2 +- 6 files changed, 36 insertions(+), 61 deletions(-) delete mode 100644 config/receiver.php diff --git a/README.md b/README.md index 42de0e5..4b2d7a4 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,35 @@ Optional: ## Configuration -Currently, configuration is only supported for the Postmark provider. To publish the configuration to your Laravel app, run this command: +Currently, configuration is only supported for the Postmark provider. To configure the Postmark integration, modify the `config/services.php` file. -```shell -php artisan vendor:publish --provider="Receiver\ReceiverServiceProvider" -``` +Add a new key called `webhook` for `postmark` configuration, and it should look like this after the modification. -A new config called `receiver.php` will be created in the `config` directory. +``` +'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + 'webhook' => [ + 'verification_types' => [ + 'auth', + 'headers', + 'ips', + ], + + 'headers' => [ + 'User-Agent' => 'Postmark', + ], + + 'ips' => [ + '3.134.147.250', + '50.31.156.6', + '50.31.156.77', + '18.217.206.57', + ], + ], +], +``` -For Postmark provider, there are three different verification types are available. +For Postmark provider, there are three different verification types are available. These checks will be run sequentially based on how they are defined in the configuration. 1. `auth` - Perform verification via the HTTP authentication. 2. `headers` - Perform verification based on the predefined key-value pair of expected headers in the config. Additional headers for the webhook can be configured directly in the Postmark dashboard. diff --git a/config/receiver.php b/config/receiver.php deleted file mode 100644 index c34821e..0000000 --- a/config/receiver.php +++ /dev/null @@ -1,41 +0,0 @@ - [ - /** - * Set the verification types to be used to verify Postmark webhook. - * The order of the verification types determines which one will be run first. - * All verification types need to pass in order for a webhook request to be verified. - * - * Supported types: "auth", "headers", "ips" - */ - 'verification_types' => [ - 'auth', - 'headers', - 'ips', - ], - - /** - * Set the combination of key-value pairs of headers to be verified against - * the webhook request. Currently, Postmark will send the "User-Agent" header with - * value of "Postmark" by default. - * - * Additional headers can be configured in the Postmark webhook management page. - */ - 'headers' => [ - 'User-Agent' => 'Postmark', - ], - - /** - * Set a list of IPs that is allowed to make the webhook request. - * By default, this options is populated with IPs provided by Postmark - * on https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks - */ - 'ips' => [ - '3.134.147.250', - '50.31.156.6', - '50.31.156.77', - '18.217.206.57', - ], - ], -]; diff --git a/src/Providers/PostmarkProvider.php b/src/Providers/PostmarkProvider.php index a3de4ba..e0e88bf 100644 --- a/src/Providers/PostmarkProvider.php +++ b/src/Providers/PostmarkProvider.php @@ -15,7 +15,7 @@ class PostmarkProvider extends AbstractProvider */ public function verify(Request $request): bool { - foreach (config('receiver.postmark.verification_types') as $verification_type) { + foreach (config('services.postmark.webhook.verification_types') ?? [] as $verification_type) { switch ($verification_type) { case 'auth': try { @@ -26,7 +26,7 @@ public function verify(Request $request): bool break; case 'headers': - foreach (config('receiver.postmark.headers') as $key => $value) { + foreach (config('services.postmark.webhook.headers') ?? [] as $key => $value) { if (!$request->hasHeader($key)) { return false; } @@ -38,7 +38,7 @@ public function verify(Request $request): bool break; case 'ips': - if (!in_array($request->getClientIp(), config('receiver.postmark.ips'), true)) { + if (!in_array($request->getClientIp(), config('services.postmark.webhook.ips') ?? [], true)) { return false; } break; diff --git a/src/ReceiverServiceProvider.php b/src/ReceiverServiceProvider.php index 5768c8b..d709a72 100644 --- a/src/ReceiverServiceProvider.php +++ b/src/ReceiverServiceProvider.php @@ -30,10 +30,6 @@ public function boot() GenerateReceiver::class, ]); } - - $this->publishes([ - __DIR__.'/../config/receiver.php' => config_path('receiver.php'), - ]); } /** diff --git a/tests/PostmarkProviderTest.php b/tests/PostmarkProviderTest.php index a828f95..b66cf3e 100644 --- a/tests/PostmarkProviderTest.php +++ b/tests/PostmarkProviderTest.php @@ -13,7 +13,7 @@ class PostmarkProviderTest extends TestCase { public function test_it_will_not_verify_postmark_webhook_when_no_verification_types_is_set() { - Config::set('receiver.postmark.verification_types', []); + Config::set('services.postmark.webhook.verification_types', []); $request = Mockery::mock(Request::class); @@ -29,7 +29,7 @@ public function test_it_will_not_verify_postmark_webhook_when_no_verification_ty public function test_it_will_verify_postmark_webhook_with_valid_headers() { - Config::set('receiver.postmark.verification_types', ['headers']); + Config::set('services.postmark.webhook.verification_types', ['headers']); $request = Mockery::mock(Request::class); @@ -48,7 +48,7 @@ public function test_it_will_verify_postmark_webhook_with_valid_headers() public function test_it_will_deny_postmark_webhook_with_missing_headers() { - Config::set('receiver.postmark.verification_types', ['headers']); + Config::set('services.postmark.webhook.verification_types', ['headers']); $this->expectException(HttpException::class); $this->expectExceptionMessage('Unauthorized'); @@ -65,7 +65,7 @@ public function test_it_will_deny_postmark_webhook_with_missing_headers() public function test_it_will_deny_postmark_webhook_with_invalid_headers() { - Config::set('receiver.postmark.verification_types', ['headers']); + Config::set('services.postmark.webhook.verification_types', ['headers']); $this->expectException(HttpException::class); $this->expectExceptionMessage('Unauthorized'); @@ -85,7 +85,7 @@ public function test_it_will_deny_postmark_webhook_with_invalid_headers() public function test_it_will_verify_postmark_webhook_with_valid_ip() { - Config::set('receiver.postmark.verification_types', ['ips']); + Config::set('services.postmark.webhook.verification_types', ['ips']); $request = Mockery::mock(Request::class); @@ -103,7 +103,7 @@ public function test_it_will_verify_postmark_webhook_with_valid_ip() public function test_it_will_deny_postmark_webhook_with_invalid_ip() { - Config::set('receiver.postmark.verification_types', ['ips']); + Config::set('services.postmark.webhook.verification_types', ['ips']); $this->expectException(HttpException::class); $this->expectExceptionMessage('Unauthorized'); diff --git a/tests/TestCase.php b/tests/TestCase.php index 6996659..dc6054a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -27,7 +27,7 @@ protected function getEnvironmentSetUp($app) 'webhook_secret' => 'slack-webhook-secret', ]); - $app['config']->set('receiver.postmark', [ + $app['config']->set('services.postmark.webhook', [ 'headers' => [ 'User-Agent' => 'Postmark', 'foo' => 'bar',