Skip to content

Commit 2fef440

Browse files
committed
Add webhooks signature verification on Sweego bridges
1 parent 5feb749 commit 2fef440

File tree

4 files changed

+94
-0
lines changed

4 files changed

+94
-0
lines changed

src/Symfony/Component/Mailer/Bridge/Sweego/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,33 @@ MAILER_DSN=sweego+api://API_KEY@default
2424
where:
2525
- `API_KEY` is your Sweego API Key
2626

27+
Webhook
28+
-------
29+
30+
Configure the webhook routing:
31+
32+
```yaml
33+
framework:
34+
webhook:
35+
routing:
36+
sweego_mailer:
37+
service: mailer.webhook.request_parser.sweego
38+
secret: '%env(SWEEGO_WEBHOOK_SECRET)%'
39+
```
40+
41+
And a consumer:
42+
43+
```php
44+
#[AsRemoteEventConsumer(name: 'sweego_mailer')]
45+
class SweegoMailEventConsumer implements ConsumerInterface
46+
{
47+
public function consume(RemoteEvent|AbstractMailerEvent $event): void
48+
{
49+
// your code
50+
}
51+
}
52+
```
53+
2754
Resources
2855
---------
2956

src/Symfony/Component/Mailer/Bridge/Sweego/Webhook/SweegoRequestParser.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher;
1617
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
1718
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
1819
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
@@ -34,6 +35,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
3435
return new ChainRequestMatcher([
3536
new MethodRequestMatcher('POST'),
3637
new IsJsonRequestMatcher(),
38+
new HeaderRequestMatcher(['webhook-id', 'webhook-timestamp', 'webhook-signature']),
3739
]);
3840
}
3941

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

56+
$this->validateSignature($request, $secret);
57+
5458
try {
5559
return $this->converter->convert($content);
5660
} catch (ParseException $e) {
5761
throw new RejectWebhookException(406, $e->getMessage(), $e);
5862
}
5963
}
64+
65+
private function validateSignature(Request $request, string $secret): void
66+
{
67+
$contentToSign = \sprintf(
68+
'%s.%s.%s',
69+
$request->headers->get('webhook-id'),
70+
$request->headers->get('webhook-timestamp'),
71+
$request->getContent(),
72+
);
73+
74+
$computedSignature = base64_encode(hash_hmac('sha256', $contentToSign, base64_decode($secret), true));
75+
76+
if (!hash_equals($computedSignature, $request->headers->get('webhook-signature'))) {
77+
throw new RejectWebhookException(403, 'Invalid signature.');
78+
}
79+
}
6080
}

src/Symfony/Component/Notifier/Bridge/Sweego/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ $sms->options($options);
4444
$texter->send($sms);
4545
```
4646

47+
Webhook
48+
-------
49+
50+
Configure the webhook routing:
51+
52+
```yaml
53+
framework:
54+
webhook:
55+
routing:
56+
sweego_sms:
57+
service: notifier.webhook.request_parser.sweego
58+
secret: '%env(SWEEGO_WEBHOOK_SECRET)%'
59+
```
60+
61+
And a consumer:
62+
63+
```php
64+
#[AsRemoteEventConsumer(name: 'sweego_sms')]
65+
class SweegoSmsEventConsumer implements ConsumerInterface
66+
{
67+
public function consume(RemoteEvent|SmsEvent $event): void
68+
{
69+
// your code
70+
}
71+
}
72+
```
73+
4774
Resources
4875
---------
4976

src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher;
1617
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
1718
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
1819
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
@@ -32,6 +33,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
3233
return new ChainRequestMatcher([
3334
new MethodRequestMatcher('POST'),
3435
new IsJsonRequestMatcher(),
36+
new HeaderRequestMatcher(['webhook-id', 'webhook-timestamp', 'webhook-signature']),
3537
]);
3638
}
3739

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

48+
$this->validateSignature($request, $secret);
49+
4650
$name = match ($payload['event_type']) {
4751
'sms_sent' => SmsEvent::DELIVERED,
4852
default => throw new RejectWebhookException(406, \sprintf('Unsupported event "%s".', $payload['event'])),
@@ -53,4 +57,20 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr
5357

5458
return $event;
5559
}
60+
61+
private function validateSignature(Request $request, string $secret): void
62+
{
63+
$contentToSign = \sprintf(
64+
'%s.%s.%s',
65+
$request->headers->get('webhook-id'),
66+
$request->headers->get('webhook-timestamp'),
67+
$request->getContent(),
68+
);
69+
70+
$computedSignature = base64_encode(hash_hmac('sha256', $contentToSign, base64_decode($secret), true));
71+
72+
if (!hash_equals($computedSignature, $request->headers->get('webhook-signature'))) {
73+
throw new RejectWebhookException(403, 'Invalid signature.');
74+
}
75+
}
5676
}

0 commit comments

Comments
 (0)