Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit9ec8b7c

Browse files
committed
feature#54525 [Mailer] [Resend] Add Resend webhook signature verification (welcoMattic)
This PR was squashed before being merged into the 7.1 branch.Discussion----------[Mailer] [Resend] Add Resend webhook signature verification| Q | A| ------------- | ---| Branch? | 7.1| Bug fix? | no| New feature? | yes| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->| Issues |Fix#53554 <!-- prefix each issue number with "Fix #", no need to create an issue if none exists, explain below instead -->| License | MITFollow up of#53554. At this time I missed webhook signature verification. To complete the Bridge before 7.1 release, here it is!I plan to add more webhook payloads in test, I asked Resend to send me example, because some are tough to reproduce.Commits-------8daa804 [Mailer] [Resend] Add Resend webhook signature verification
2 parents22cbf8f +8daa804 commit9ec8b7c

File tree

6 files changed

+156
-4
lines changed

6 files changed

+156
-4
lines changed

‎src/Symfony/Component/Mailer/Bridge/Resend/Tests/ResendApiTransportTest.php‎renamed to ‎src/Symfony/Component/Mailer/Bridge/Resend/Tests/Transport/ResendApiTransportTest.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespaceSymfony\Component\Mailer\Bridge\Resend\Tests;
12+
namespaceSymfony\Component\Mailer\Bridge\Resend\Tests\Transport;
1313

1414
usePHPUnit\Framework\TestCase;
1515
useSymfony\Component\HttpClient\MockHttpClient;

‎src/Symfony/Component/Mailer/Bridge/Resend/Tests/ResendTransportFactoryTest.php‎renamed to ‎src/Symfony/Component/Mailer/Bridge/Resend/Tests/Transport/ResendTransportFactoryTest.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespaceSymfony\Component\Mailer\Bridge\Resend\Tests;
12+
namespaceSymfony\Component\Mailer\Bridge\Resend\Tests\Transport;
1313

1414
usePsr\Log\NullLogger;
1515
useSymfony\Component\HttpClient\MockHttpClient;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"created_at":"2024-04-08T09:43:09.500Z",
3+
"data": {
4+
"created_at":"2024-04-08T09:43:09.438Z",
5+
"email_id":"172c41ce-ba6d-4281-8a7a-541faa725748",
6+
"from":"test@resend.com",
7+
"headers": [
8+
{
9+
"name":"Sender",
10+
"value":"test@resend.com"
11+
}
12+
],
13+
"subject":"Test subject",
14+
"to": [
15+
"test@example.com"
16+
]
17+
},
18+
"type":"email.sent"
19+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
useSymfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent;
4+
5+
$wh =newMailerDeliveryEvent(MailerDeliveryEvent::RECEIVED,'172c41ce-ba6d-4281-8a7a-541faa725748',json_decode(file_get_contents(str_replace('.php','.json',__FILE__)),true));
6+
$wh->setRecipientEmail('test@example.com');
7+
$wh->setTags([]);
8+
$wh->setMetadata([
9+
'created_at' =>'2024-04-08T09:43:09.438Z',
10+
'email_id' =>'172c41ce-ba6d-4281-8a7a-541faa725748',
11+
'from' =>'test@resend.com',
12+
'headers' => [
13+
[
14+
'name' =>'Sender',
15+
'value' =>'test@resend.com'
16+
],
17+
],
18+
'subject' =>'Test subject',
19+
'to' => [
20+
'test@example.com',
21+
],
22+
]);
23+
$wh->setReason('');
24+
$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z','2024-04-08T09:43:09.500000Z'));
25+
26+
return$wh;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespaceSymfony\Component\Mailer\Bridge\Resend\Tests\Webhook;
13+
14+
useSymfony\Component\HttpFoundation\Request;
15+
useSymfony\Component\Mailer\Bridge\Resend\RemoteEvent\ResendPayloadConverter;
16+
useSymfony\Component\Mailer\Bridge\Resend\Webhook\ResendRequestParser;
17+
useSymfony\Component\Webhook\Client\RequestParserInterface;
18+
useSymfony\Component\Webhook\Test\AbstractRequestParserTestCase;
19+
20+
class ResendRequestParserTestextends AbstractRequestParserTestCase
21+
{
22+
protectedfunctioncreateRequestParser():RequestParserInterface
23+
{
24+
returnnewResendRequestParser(newResendPayloadConverter());
25+
}
26+
27+
protectedfunctiongetSecret():string
28+
{
29+
return'whsec_ESwTAuuIe3yfH4DgdgI+ENsiNzPAGdp+';
30+
}
31+
32+
protectedfunctioncreateRequest(string$payload):Request
33+
{
34+
return Request::create('/','POST', [], [], [], [
35+
'Content-Type' =>'application/json',
36+
'HTTP_svix-id' =>'172c41ce-ba6d-4281-8a7a-541faa725748',
37+
'HTTP_svix-timestamp' =>'1712569389',
38+
'HTTP_svix-signature' =>'v1,4wjuRp64yC/2itgCQwl2xPePVwSPTdPbXLIY6IxGLTA=',
39+
],str_replace("\n","\r\n",$payload));
40+
}
41+
}

‎src/Symfony/Component/Mailer/Bridge/Resend/Webhook/ResendRequestParser.php‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
namespaceSymfony\Component\Mailer\Bridge\Resend\Webhook;
1313

1414
useSymfony\Component\HttpFoundation\ChainRequestMatcher;
15+
useSymfony\Component\HttpFoundation\HeaderBag;
1516
useSymfony\Component\HttpFoundation\Request;
17+
useSymfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher;
1618
useSymfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
1719
useSymfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
18-
useSymfony\Component\HttpFoundation\RequestMatcher\SchemeRequestMatcher;
1920
useSymfony\Component\HttpFoundation\RequestMatcherInterface;
2021
useSymfony\Component\Mailer\Bridge\Resend\RemoteEvent\ResendPayloadConverter;
22+
useSymfony\Component\Mailer\Exception\InvalidArgumentException;
2123
useSymfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent;
2224
useSymfony\Component\RemoteEvent\Exception\ParseException;
2325
useSymfony\Component\Webhook\Client\AbstractRequestParser;
@@ -34,14 +36,23 @@ protected function getRequestMatcher(): RequestMatcherInterface
3436
{
3537
returnnewChainRequestMatcher([
3638
newMethodRequestMatcher('POST'),
37-
newSchemeRequestMatcher('https'),
3839
newIsJsonRequestMatcher(),
40+
newHeaderRequestMatcher([
41+
'svix-id',
42+
'svix-timestamp',
43+
'svix-signature',
44+
]),
3945
]);
4046
}
4147

4248
protectedfunctiondoParse(Request$request, #[\SensitiveParameter]string$secret): ?AbstractMailerEvent
4349
{
50+
if (!$secret) {
51+
thrownewInvalidArgumentException('A non-empty secret is required.');
52+
}
53+
4454
$content =$request->toArray();
55+
4556
if (
4657
!isset($content['type'])
4758
|| !isset($content['created_at'])
@@ -55,10 +66,65 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr
5566
thrownewRejectWebhookException(406,'Payload is malformed.');
5667
}
5768

69+
$this->validateSignature($request->getContent(),$request->headers,$secret);
70+
5871
try {
5972
return$this->converter->convert($content);
6073
}catch (ParseException$e) {
6174
thrownewRejectWebhookException(406,$e->getMessage(),$e);
6275
}
6376
}
77+
78+
privatefunctionvalidateSignature(string$payload,HeaderBag$headers,string$secret):void
79+
{
80+
$secret =$this->decodeSecret($secret);
81+
$messageId =$headers->get('svix-id');
82+
$messageTimestamp = (int)$headers->get('svix-timestamp');
83+
$messageSignature =$headers->get('svix-signature');
84+
85+
$signature =$this->sign($secret,$messageId,$messageTimestamp,$payload);
86+
$expectedSignature =explode(',',$signature,2)[1];
87+
$passedSignatures =explode('',$messageSignature);
88+
$signatureFound =false;
89+
90+
foreach ($passedSignaturesas$versionedSignature) {
91+
$signatureParts =explode(',',$versionedSignature,2);
92+
$version =$signatureParts[0];
93+
94+
if ('v1' !==$version) {
95+
continue;
96+
}
97+
98+
$passedSignature =$signatureParts[1];
99+
100+
if (hash_equals($expectedSignature,$passedSignature)) {
101+
$signatureFound =true;
102+
103+
break;
104+
}
105+
}
106+
107+
if (!$signatureFound) {
108+
thrownewRejectWebhookException(406,'No signatures found matching the expected signature.');
109+
}
110+
}
111+
112+
privatefunctionsign(string$secret,string$messageId,int$timestamp,string$payload):string
113+
{
114+
$toSign =sprintf('%s.%s.%s',$messageId,$timestamp,$payload);
115+
$hash =hash_hmac('sha256',$toSign,$secret);
116+
$signature =base64_encode(pack('H*',$hash));
117+
118+
return'v1,'.$signature;
119+
}
120+
121+
privatefunctiondecodeSecret(string$secret):string
122+
{
123+
$prefix ='whsec_';
124+
if (str_starts_with($secret,$prefix)) {
125+
$secret =substr($secret,\strlen($prefix));
126+
}
127+
128+
returnbase64_decode($secret);
129+
}
64130
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp