Skip to content

Commit

Permalink
Add webhooks signature verification on Sweego bridges
Browse files Browse the repository at this point in the history
  • Loading branch information
welcoMattic committed Dec 4, 2024
1 parent 9b0ca99 commit 88769e8
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function matches(Request $request): bool
{
foreach ($this->matchers as $matcher) {
if (!$matcher->matches($request)) {
dump($request);
return false;
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/Symfony/Component/Mailer/Bridge/Sweego/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ MAILER_DSN=sweego+api://API_KEY@default
where:
- `API_KEY` is your Sweego API Key

Webhook
-------

Configure the webhook routing:

```yaml
framework:
webhook:
routing:
sweego_mailer:
service: mailer.webhook.request_parser.sweego
secret: '%env(SWEEGO_WEBHOOK_SECRET)%'
```
And a consumer:
```php
#[AsRemoteEventConsumer(name: 'sweego_mailer')]
class SweegoMailEventConsumer implements ConsumerInterface
{
public function consume(RemoteEvent|AbstractMailerEvent $event): void
{
// your code
}
}
```

Resources
---------

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_webhook-id' => '9f26b9d0-13d7-410c-ba04-5019cd30e6d0',
'HTTP_webhook-timestamp' => '1723737959',
'HTTP_webhook-signature' => 'W+fm4VPshCGjuT0HxyV00QEbFitZd2Rdvx82bWM7VXc=',
], $payload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sweego\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Bridge\Sweego\RemoteEvent\SweegoPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sweego\Webhook\SweegoRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

class SweegoWrongSignatureRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Invalid signature.');

return new SweegoRequestParser(new SweegoPayloadConverter());
}

protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_webhook-id' => '9f26b9d0-13d7-410c-ba04-5019cd30e6d0',
'HTTP_webhook-timestamp' => '1723737959',
'HTTP_webhook-signature' => 'wrong_signature',
], $payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
Expand All @@ -34,6 +35,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
return new ChainRequestMatcher([
new MethodRequestMatcher('POST'),
new IsJsonRequestMatcher(),
new HeaderRequestMatcher(['webhook-id', 'webhook-timestamp', 'webhook-signature']),
]);
}

Expand All @@ -51,10 +53,28 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr
throw new RejectWebhookException(406, 'Payload is malformed.');
}

$this->validateSignature($request, $secret);

try {
return $this->converter->convert($content);
} catch (ParseException $e) {
throw new RejectWebhookException(406, $e->getMessage(), $e);
}
}

private function validateSignature(Request $request, string $secret): void
{
$contentToSign = \sprintf(
'%s.%s.%s',
$request->headers->get('webhook-id'),
$request->headers->get('webhook-timestamp'),
$request->getContent(),
);

$computedSignature = base64_encode(hash_hmac('sha256', $contentToSign, base64_decode($secret), true));

if (!hash_equals($computedSignature, $request->headers->get('webhook-signature'))) {
throw new RejectWebhookException(403, 'Invalid signature.');
}
}
}
27 changes: 27 additions & 0 deletions src/Symfony/Component/Notifier/Bridge/Sweego/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ $sms->options($options);
$texter->send($sms);
```

Webhook
-------

Configure the webhook routing:

```yaml
framework:
webhook:
routing:
sweego_sms:
service: notifier.webhook.request_parser.sweego
secret: '%env(SWEEGO_WEBHOOK_SECRET)%'
```
And a consumer:
```php
#[AsRemoteEventConsumer(name: 'sweego_sms')]
class SweegoSmsEventConsumer implements ConsumerInterface
{
public function consume(RemoteEvent|SmsEvent $event): void
{
// your code
}
}
```

Resources
---------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Notifier\Bridge\Sweego\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;
Expand All @@ -21,4 +22,14 @@ protected function createRequestParser(): RequestParserInterface
{
return new SweegoRequestParser();
}

protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_webhook-id' => 'a5ccc627-6e43-4012-bb29-f1bfe3a3d13e',
'HTTP_webhook-timestamp' => '1725290740',
'HTTP_webhook-signature' => 'k7SwzHXZqVKNvCpp6HwGS/5aDZ6NraYnKmVkBdx7MHE=',
], $payload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Notifier\Bridge\Sweego\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

class SweegoWrongSignatureRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Invalid signature.');

return new SweegoRequestParser();
}

protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_webhook-id' => 'a5ccc627-6e43-4012-bb29-f1bfe3a3d13e',
'HTTP_webhook-timestamp' => '1725290740',
'HTTP_webhook-signature' => 'wrong_signature',
], $payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
Expand All @@ -32,6 +33,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
return new ChainRequestMatcher([
new MethodRequestMatcher('POST'),
new IsJsonRequestMatcher(),
new HeaderRequestMatcher(['webhook-id', 'webhook-timestamp', 'webhook-signature']),
]);
}

Expand All @@ -43,6 +45,8 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr
throw new RejectWebhookException(406, 'Payload is malformed.');
}

$this->validateSignature($request, $secret);

$name = match ($payload['event_type']) {
'sms_sent' => SmsEvent::DELIVERED,
default => throw new RejectWebhookException(406, \sprintf('Unsupported event "%s".', $payload['event'])),
Expand All @@ -53,4 +57,20 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr

return $event;
}

private function validateSignature(Request $request, string $secret): void
{
$contentToSign = \sprintf(
'%s.%s.%s',
$request->headers->get('webhook-id'),
$request->headers->get('webhook-timestamp'),
$request->getContent(),
);

$computedSignature = base64_encode(hash_hmac('sha256', $contentToSign, base64_decode($secret), true));

if (!hash_equals($computedSignature, $request->headers->get('webhook-signature'))) {
throw new RejectWebhookException(403, 'Invalid signature.');
}
}
}

0 comments on commit 88769e8

Please sign in to comment.