12
12
namespace Symfony \Component \Mailer \Bridge \Resend \Webhook ;
13
13
14
14
use Symfony \Component \HttpFoundation \ChainRequestMatcher ;
15
+ use Symfony \Component \HttpFoundation \HeaderBag ;
15
16
use Symfony \Component \HttpFoundation \Request ;
16
17
use Symfony \Component \HttpFoundation \RequestMatcher \IsJsonRequestMatcher ;
17
18
use Symfony \Component \HttpFoundation \RequestMatcher \MethodRequestMatcher ;
18
- use Symfony \Component \HttpFoundation \RequestMatcher \SchemeRequestMatcher ;
19
19
use Symfony \Component \HttpFoundation \RequestMatcherInterface ;
20
20
use Symfony \Component \Mailer \Bridge \Resend \RemoteEvent \ResendPayloadConverter ;
21
+ use Symfony \Component \Mailer \Exception \InvalidArgumentException ;
21
22
use Symfony \Component \RemoteEvent \Event \Mailer \AbstractMailerEvent ;
22
23
use Symfony \Component \RemoteEvent \Exception \ParseException ;
23
24
use Symfony \Component \Webhook \Client \AbstractRequestParser ;
@@ -34,14 +35,18 @@ protected function getRequestMatcher(): RequestMatcherInterface
34
35
{
35
36
return new ChainRequestMatcher ([
36
37
new MethodRequestMatcher ('POST ' ),
37
- new SchemeRequestMatcher ('https ' ),
38
38
new IsJsonRequestMatcher (),
39
39
]);
40
40
}
41
41
42
42
protected function doParse (Request $ request , #[\SensitiveParameter] string $ secret ): ?AbstractMailerEvent
43
43
{
44
+ if (!$ secret ) {
45
+ throw new InvalidArgumentException ('A non-empty secret is required. ' );
46
+ }
47
+
44
48
$ content = $ request ->toArray ();
49
+
45
50
if (
46
51
!isset ($ content ['type ' ])
47
52
|| !isset ($ content ['created_at ' ])
@@ -55,10 +60,70 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr
55
60
throw new RejectWebhookException (406 , 'Payload is malformed. ' );
56
61
}
57
62
63
+ $ this ->validateSignature ($ request ->getContent (), $ request ->headers , $ secret );
64
+
58
65
try {
59
66
return $ this ->converter ->convert ($ content );
60
67
} catch (ParseException $ e ) {
61
68
throw new RejectWebhookException (406 , $ e ->getMessage (), $ e );
62
69
}
63
70
}
71
+
72
+ private function validateSignature (string $ payload , HeaderBag $ headers , string $ secret ): void
73
+ {
74
+ $ secret = $ this ->decodeSecret ($ secret );
75
+
76
+ if ($ headers ->has ('svix-id ' ) && $ headers ->has ('svix-timestamp ' ) && $ headers ->has ('svix-signature ' )) {
77
+ $ messageId = $ headers ->get ('svix-id ' );
78
+ $ messageTimestamp = (int ) $ headers ->get ('svix-timestamp ' );
79
+ $ messageSignature = $ headers ->get ('svix-signature ' );
80
+ } else {
81
+ throw new RejectWebhookException (406 , 'Missing required headers. ' );
82
+ }
83
+
84
+ $ signature = $ this ->sign ($ secret , $ messageId , $ messageTimestamp , $ payload );
85
+ $ expectedSignature = explode (', ' , $ signature , 2 )[1 ];
86
+ $ passedSignatures = explode (' ' , $ messageSignature );
87
+ $ signatureFound = false ;
88
+
89
+ foreach ($ passedSignatures as $ versionedSignature ) {
90
+ $ signatureParts = explode (', ' , $ versionedSignature , 2 );
91
+ $ version = $ signatureParts [0 ];
92
+
93
+ if ('v1 ' !== $ version ) {
94
+ continue ;
95
+ }
96
+
97
+ $ passedSignature = $ signatureParts [1 ];
98
+
99
+ if (hash_equals ($ expectedSignature , $ passedSignature )) {
100
+ $ signatureFound = true ;
101
+
102
+ break ;
103
+ }
104
+ }
105
+
106
+ if (!$ signatureFound ) {
107
+ throw new RejectWebhookException (406 , 'No signatures found matching the expected signature. ' );
108
+ }
109
+ }
110
+
111
+ private function sign (string $ secret , string $ messageId , int $ timestamp , string $ payload ): string
112
+ {
113
+ $ toSign = sprintf ('%s.%s.%s ' , $ messageId , $ timestamp , $ payload );
114
+ $ hash = hash_hmac ('sha256 ' , $ toSign , $ secret );
115
+ $ signature = base64_encode (pack ('H* ' , $ hash ));
116
+
117
+ return 'v1, ' .$ signature ;
118
+ }
119
+
120
+ private function decodeSecret (string $ secret ): string
121
+ {
122
+ $ prefix = 'whsec_ ' ;
123
+ if (str_starts_with ($ secret , $ prefix )) {
124
+ $ secret = substr ($ secret , \strlen ($ prefix ));
125
+ }
126
+
127
+ return base64_decode ($ secret );
128
+ }
64
129
}
0 commit comments