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 Oct 24, 2024
1 parent 34e8a96 commit 00f4dcb
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 2 deletions.
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:
service: notifier.webhook.request_parser.sweego
secret: '%env(SWEEGO_WEBHOOK_SECRET)%'
```
And a consumer:
```php
#[AsRemoteEventConsumer(name: 'sweego')]
class SweegoConsumer 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 @@ -123,7 +123,7 @@ private function setCampaignType(array $body, array $options): array
{
$body['campaign-type'] = $this->campaignType;

if (!\array_key_exists(SweegoOptions::CAMPAIGN_TYPE, $options) && \is_string($options[SweegoOptions::CAMPAIGN_TYPE])) {
if (\array_key_exists(SweegoOptions::CAMPAIGN_TYPE, $options) && \is_string($options[SweegoOptions::CAMPAIGN_TYPE])) {
$body['campaign-type'] = $options[SweegoOptions::CAMPAIGN_TYPE];
}

Expand All @@ -134,7 +134,7 @@ private function setCampaignId(array $body, array $options): array
{
$body['campaign-id'] = $this->campaignId;

if (!\array_key_exists(SweegoOptions::CAMPAIGN_ID, $options) && \is_string($options[SweegoOptions::CAMPAIGN_ID])) {
if (\array_key_exists(SweegoOptions::CAMPAIGN_ID, $options) && \is_string($options[SweegoOptions::CAMPAIGN_ID])) {
$body['campaign-id'] = $options[SweegoOptions::CAMPAIGN_ID];
}

Expand Down
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,21 @@ 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 00f4dcb

Please sign in to comment.