Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional verification method to the Postmark provider #31

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -53,6 +54,42 @@ Optional:

**Stripe** support requires [`stripe/stripe-php`](https://github.com/stripe/stripe-php)

## Configuration

Currently, configuration is only supported for the Postmark provider. To configure the Postmark integration, modify the `config/services.php` file.

Add a new key called `webhook` for `postmark` configuration, and it should look like this after the modification.

```
'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. 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.
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
Expand Down
34 changes: 29 additions & 5 deletions src/Providers/PostmarkProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,37 @@ class PostmarkProvider extends AbstractProvider
*/
public function verify(Request $request): bool
{
try {
Auth::onceBasic();
foreach (config('services.postmark.webhook.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('services.postmark.webhook.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('services.postmark.webhook.ips') ?? [], true)) {
return false;
}
break;
}
}

return true;
}

/**
Expand Down
158 changes: 158 additions & 0 deletions tests/PostmarkProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

namespace Receiver\Tests;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Mockery;
use Receiver\Providers\PostmarkProvider;
use Receiver\Providers\Webhook;
use Symfony\Component\HttpKernel\Exception\HttpException;

class PostmarkProviderTest extends TestCase
{
public function test_it_will_not_verify_postmark_webhook_when_no_verification_types_is_set()
{
Config::set('services.postmark.webhook.verification_types', []);

$request = Mockery::mock(Request::class);

$this->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('services.postmark.webhook.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('services.postmark.webhook.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_deny_postmark_webhook_with_invalid_headers()
{
Config::set('services.postmark.webhook.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('services.postmark.webhook.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('services.postmark.webhook.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;
}
}
10 changes: 10 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ protected function getEnvironmentSetUp($app)
'redirect' => 'http://your-callback-url',
'webhook_secret' => 'slack-webhook-secret',
]);

$app['config']->set('services.postmark.webhook', [
'headers' => [
'User-Agent' => 'Postmark',
'foo' => 'bar',
],
'ips' => [
'123.123.123.123',
],
]);
}

/**
Expand Down