Skip to content

Commit 06493b7

Browse files
Merge pull request #120 from square/release/29.1.0.20230720
Generated PR for Release: 29.1.0.20230720
2 parents 6b8fab3 + 2c721fa commit 06493b7

File tree

4 files changed

+191
-3
lines changed

4 files changed

+191
-3
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "square/square",
33
"description": "Use Square APIs to manage and run business including payment, customer, product, inventory, and employee management.",
4-
"version": "29.0.0.20230720",
4+
"version": "29.1.0.20230720",
55
"type": "library",
66
"keywords": [
77
"Square",

src/SquareClient.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public function __construct(array $config = [])
162162
->jsonHelper(ApiHelper::getJsonHelper())
163163
->apiCallback($this->config['httpCallback'] ?? null)
164164
->userAgent(
165-
'Square-PHP-SDK/29.0.0.20230720 ({api-version}) {engine}/{engine-version} ({os-' .
165+
'Square-PHP-SDK/29.1.0.20230720 ({api-version}) {engine}/{engine-version} ({os-' .
166166
'info}) {detail}'
167167
)
168168
->userAgentConfig(
@@ -304,7 +304,7 @@ public function withConfiguration(array $config): self
304304
*/
305305
public function getSdkVersion(): string
306306
{
307-
return '29.0.0.20230720';
307+
return '29.1.0.20230720';
308308
}
309309

310310
/**

src/Utils/WebhooksHelper.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
namespace Square\Utils;
3+
4+
use Exception;
5+
6+
/**
7+
* Utility to help with Square Webhooks
8+
*/
9+
class WebhooksHelper {
10+
/**
11+
* Verifies and validates an event notification.
12+
* See the documentation for more details.
13+
*
14+
* @param string $requestBody The JSON body of the request.
15+
* @param string $signatureHeader The value for the `x-square-hmacsha256-signature` header.
16+
* @param string $signatureKey The signature key from the Square Developer portal for the webhook subscription.
17+
* @param string $notificationUrl The notification endpoint URL as defined in the Square Developer portal for the webhook subscription.
18+
* @return bool `true` if the signature is valid, indicating that the event can be trusted as it came from Square. `false` if the signature validation fails, indicating that the event did not come from Square, so it may be malicious and should be discarded.
19+
* @throws Exception If the signatureKey or notificationUrl is null or empty.
20+
*/
21+
public static function isValidWebhookEventSignature(
22+
string $requestBody,
23+
string $signatureHeader,
24+
string $signatureKey,
25+
string $notificationUrl
26+
): bool {
27+
28+
if ($requestBody === null) {
29+
return false;
30+
}
31+
32+
if ($signatureKey === null || strlen($signatureKey) === 0) {
33+
throw new Exception('signatureKey is null or empty');
34+
}
35+
if ($notificationUrl === null || strlen($notificationUrl) === 0) {
36+
throw new Exception('notificationUrl is null or empty');
37+
}
38+
39+
// Perform UTF-8 encoding to bytes
40+
$payload = $notificationUrl . $requestBody;
41+
$payloadBytes = mb_convert_encoding($payload, 'UTF-8');
42+
$signatureKeyBytes = mb_convert_encoding($signatureKey, 'UTF-8');
43+
44+
// Compute the hash value
45+
$hash = hash_hmac('sha256', $payloadBytes, $signatureKeyBytes, true);
46+
47+
// Compare the computed hash vs the value in the signature header
48+
$hashBase64 = base64_encode($hash);
49+
50+
return $hashBase64 === $signatureHeader;
51+
}
52+
}

tests/Flows/WebhooksHelperTest.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
namespace Square\Tests;
3+
4+
use Square\Utils\WebhooksHelper;
5+
use PHPUnit\Framework\TestCase;
6+
7+
class WebhooksHelperTest extends TestCase
8+
{
9+
private $requestBody = '{"merchant_id":"MLEFBHHSJGVHD","type":"webhooks.test_notification","event_id":"ac3ac95b-f97d-458c-a6e6-18981597e05f","created_at":"2022-07-13T20:30:59.037339943Z","data":{"type":"webhooks","id":"bc368e64-01aa-407e-b46e-3231809b1129"}}';
10+
private $signatureHeader = 'GF4YkrJgGBDZ9NIYbNXBnMzqb2HoL4RW/S6vkZ9/2N4=';
11+
private $signatureKey = 'Ibxx_5AKakO-3qeNVR61Dw';
12+
private $notificationUrl = 'https://webhook.site/679a4f3a-dcfa-49ee-bac5-9d0edad886b9';
13+
14+
public function testSignatureValidationPass(): void
15+
{
16+
$isValid = WebhooksHelper::isValidWebhookEventSignature(
17+
$this->requestBody,
18+
$this->signatureHeader,
19+
$this->signatureKey,
20+
$this->notificationUrl
21+
);
22+
$this->assertTrue($isValid);
23+
}
24+
25+
public function testSignatureValidationFailsOnNotificationUrlMismatch(): void
26+
{
27+
$isValid = WebhooksHelper::isValidWebhookEventSignature(
28+
$this->requestBody,
29+
$this->signatureHeader,
30+
$this->signatureKey,
31+
'https://webhook.site/79a4f3a-dcfa-49ee-bac5-9d0edad886b9'
32+
);
33+
$this->assertFalse($isValid);
34+
}
35+
36+
public function testSignatureValidationFailsOnInvalidSignatureKey(): void
37+
{
38+
$isValid = WebhooksHelper::isValidWebhookEventSignature(
39+
$this->requestBody,
40+
$this->signatureHeader,
41+
'MCmhFRxGX82xMwg5FsaoQA',
42+
$this->notificationUrl
43+
);
44+
$this->assertFalse($isValid);
45+
}
46+
47+
public function testSignatureValidationFailsOnInvalidSignatureHeader(): void
48+
{
49+
$isValid = WebhooksHelper::isValidWebhookEventSignature(
50+
$this->requestBody,
51+
'1z2n3DEJJiUPKcPzQo1ftvbxGdw=',
52+
$this->signatureKey,
53+
$this->notificationUrl
54+
);
55+
$this->assertFalse($isValid);
56+
}
57+
58+
public function testSignatureValidationFailsOnRequestBodyMismatch(): void
59+
{
60+
$requestBody = '{"merchant_id":"MLEFBHHSJGVHD","type":"webhooks.test_notification","event_id":"ac3ac95b-f97d-458c-a6e6-18981597e05f","created_at":"2022-06-13T20:30:59.037339943Z","data":{"type":"webhooks","id":"bc368e64-01aa-407e-b46e-3231809b1129"}}';
61+
$isValid = WebhooksHelper::isValidWebhookEventSignature(
62+
$requestBody,
63+
$this->signatureHeader,
64+
$this->signatureKey,
65+
$this->notificationUrl
66+
);
67+
$this->assertFalse($isValid);
68+
}
69+
70+
public function testSignatureValidationFailsOnRequestBodyFormatted(): void
71+
{
72+
$requestBody = '{
73+
"merchant_id": "MLEFBHHSJGVHD",
74+
"type": "webhooks.test_notification",
75+
"event_id": "ac3ac95b-f97d-458c-a6e6-18981597e05f",
76+
"created_at": "2022-07-13T20:30:59.037339943Z",
77+
"data": {
78+
"type": "webhooks",
79+
"id": "bc368e64-01aa-407e-b46e-3231809b1129"
80+
}
81+
}';
82+
$isValid = WebhooksHelper::isValidWebhookEventSignature(
83+
$requestBody,
84+
$this->signatureHeader,
85+
$this->signatureKey,
86+
$this->notificationUrl
87+
);
88+
$this->assertFalse($isValid);
89+
}
90+
91+
public function testThrowsErrorOnEmptySignatureKey(): void
92+
{
93+
$this->expectExceptionMessage('signatureKey is null or empty');
94+
WebhooksHelper::isValidWebhookEventSignature(
95+
$this->requestBody,
96+
$this->signatureHeader,
97+
'',
98+
$this->notificationUrl
99+
);
100+
}
101+
102+
public function testThrowsErrorOnSignatureKeyNotConfigured(): void
103+
{
104+
$this->expectException(\TypeError::class);
105+
106+
WebhooksHelper::isValidWebhookEventSignature(
107+
$this->requestBody,
108+
$this->signatureHeader,
109+
null,
110+
$this->notificationUrl
111+
);
112+
}
113+
114+
public function testThrowsErrorOnEmptyNotificationUrl(): void
115+
{
116+
$this->expectExceptionMessage('notificationUrl is null or empty');
117+
WebhooksHelper::isValidWebhookEventSignature(
118+
$this->requestBody,
119+
$this->signatureHeader,
120+
$this->signatureKey,
121+
''
122+
);
123+
}
124+
125+
public function testThrowsErrorOnNotificationUrlNotConfigured(): void
126+
{
127+
$this->expectException(\TypeError::class);
128+
129+
WebhooksHelper::isValidWebhookEventSignature(
130+
$this->requestBody,
131+
$this->signatureHeader,
132+
$this->signatureKey,
133+
null
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)