Skip to content

Commit d52941d

Browse files
Add method for callback validation
1 parent d54f8ee commit d52941d

2 files changed

Lines changed: 96 additions & 5 deletions

File tree

src/SettleApiClient.php

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace Danielz\SettleApi;
44

5-
use Exception;
6-
75
class SettleApiClient
86
{
97
protected string $merchantId;
@@ -15,10 +13,12 @@ class SettleApiClient
1513
const BASE_URL_PRODUCTION = 'https://api.settle.eu/merchant/v1/';
1614
const BASE_URL_SANDBOX = 'https://api.sandbox.settle.eu/merchant/v1/';
1715

16+
const PUBLIC_KEY_PRODUCTION = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9iglTBPG1poCw3qFPlxT0MSHO\nt6kgRmpVrLBY9Fx8Zn+zAoY89ZeFhwwnRR8IDQcj4yEAjsoXCxtH3bbh/OdvlFG6\nxdSsAeph6/MSk9YAVKWRWU5ber9cgoQ89KJ14goLUnhhegynUjnz+hdgAET5k9Uc\nsxnmfU7XeT78FP02JQIDAQAB\n-----END PUBLIC KEY-----";
17+
const PUBLIC_KEY_SANDBOX = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDS92fCQmAPDpmcgraqPRXgz4Nd\nd/biPxIH5aG1dAQ8dMMcEjGCn7Sm5VcX1iV8L5oW+MlcnHFaZdVyy1Lcqed/8+r0\nQM9cFqQWif35C+eOr/s7/CCY/WXMApqO6YihtHvP+jgjrXltw0LHrUwMWO718udN\nhlg22QkpjhG90kvf3QIDAQAB\n-----END PUBLIC KEY-----";
1818

1919
const SETTLE_LINK = 'https://settle.eu';
20-
const PAYMENT_LINK = 'https://settle.eu/p/:payment_request_id/';
21-
const PAYMENT_LINK_MOBILE = 'https://settle.eu/p/:payment_request_id/';
20+
const PAYMENT_LINK = 'http://settle.eu/p/:payment_request_id/';
21+
const PAYMENT_LINK_MOBILE = 'http://settle.eu/p/:payment_request_id/';
2222
const PAYMENT_LINK_MOBILE_SANDBOX = 'https://settledemo.page.link/?apn=eu.settle.app.sandbox&ibi=eu.settle.app.sandbox&isi=1453180781&ius=eu.settle.app.firebaselink&link=https://settle-demo://qr/http://settle.eu/p/:payment_request_id/';
2323

2424
/**
@@ -46,6 +46,14 @@ public function getIsSandbox()
4646
return $this->isSandbox;
4747
}
4848

49+
/**
50+
* @return string
51+
*/
52+
public function getSettlePublicKey()
53+
{
54+
return $this->isSandbox ? self::PUBLIC_KEY_SANDBOX : self::PUBLIC_KEY_PRODUCTION;
55+
}
56+
4957
/**
5058
* @param $isSandbox
5159
*/
@@ -151,6 +159,74 @@ protected function getHeaders(string $method, string $url, array $postFields): a
151159
return $headers;
152160
}
153161

162+
163+
/**
164+
* @param string $callbackUrl
165+
* @param string $method
166+
* @return false|mixed
167+
* @throws SettleApiException
168+
*/
169+
public function getCallbackData($callbackUrl = '', $method = 'POST')
170+
{
171+
$body = file_get_contents('php://input');
172+
if (empty($callbackUrl)) {
173+
$callbackUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
174+
}
175+
176+
$is_valid_request = $this->isValidCallback($callbackUrl, $body, $_SERVER, $method);
177+
178+
return $is_valid_request ? json_decode($body, true) : false;
179+
}
180+
181+
/**
182+
* @param string $callbackUrl
183+
* @param string|array $body
184+
* @param array $headers
185+
* @param string $method
186+
* @return bool
187+
* @throws SettleApiException
188+
*/
189+
public function isValidCallback($callbackUrl, $body, $headers, $method = 'POST')
190+
{
191+
$data = is_string($body) ? $body : json_encode($body);
192+
$content_digest = base64_encode(hash('sha256', $data, true));
193+
194+
$expected_signature = false;
195+
$expected_content_digest = false;
196+
$settle_headers = [];
197+
foreach($headers as $header => $value) {
198+
$normalized_header = str_replace(['http_', '_'], ['', '-'], strtolower($header));
199+
switch($normalized_header) {
200+
case 'authorization':
201+
list($algo, $authorization) = explode(' ', $value, 2);
202+
if ($algo == 'RSA-SHA256') {
203+
$expected_signature = base64_decode($authorization);
204+
}
205+
break;
206+
case 'x-settle-timestamp':
207+
$settle_headers[] = 'X-SETTLE-TIMESTAMP=' . $value;
208+
break;
209+
case 'x-settle-content-digest':
210+
list($algo, $digest) = explode('=', $value, 2);
211+
if ($algo == 'SHA256') {
212+
$settle_headers[] = 'X-SETTLE-CONTENT-DIGEST=' . $value;
213+
$expected_content_digest = $digest;
214+
}
215+
break;
216+
}
217+
}
218+
sort($settle_headers);
219+
220+
if (!$expected_signature) {
221+
throw new SettleApiException("Authorization header is missing.");
222+
}
223+
224+
$fingerprint = join('|', [strtoupper($method), $callbackUrl, join('&', $settle_headers)]);
225+
$valid_signature = openssl_verify($fingerprint, $expected_signature, $this->getSettlePublicKey(), OPENSSL_ALGO_SHA256);
226+
227+
return ($expected_content_digest == $content_digest) && $valid_signature;
228+
}
229+
154230
public function createLink($template, array $data = [])
155231
{
156232
switch($template) {

tests/ApiTest.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
$api_client->setIsSandbox($isSandbox);
187187

188188
$api = $merchant_api->payment_requests;
189-
expect($api->getPaymentLink('pcqghkrpztq1'))->toBe('https://settle.eu/p/pcqghkrpztq1/');
189+
expect($api->getPaymentLink('pcqghkrpztq1'))->toBe('http://settle.eu/p/pcqghkrpztq1/');
190190
expect($api->getMobilePaymentLink('pcqghkrpztq1'))->toBe('https://settledemo.page.link/?apn=eu.settle.app.sandbox&ibi=eu.settle.app.sandbox&isi=1453180781&ius=eu.settle.app.firebaselink&link=https://settle-demo://qr/http://settle.eu/p/pcqghkrpztq1/');
191191
});
192192

@@ -201,3 +201,18 @@
201201
expect($e->getCode())->toBe(404);
202202
}
203203
});
204+
205+
test('Callback validation', function () {
206+
global $api_client;
207+
208+
$body = '{"meta": {"seqno": 0, "labels": ["timeline"], "uri": "https://api-dot-settle-core-demo.appspot.com/merchant/v1/payment_request/phkan3yvn4ex/outcome/", "id": "daMUL1Q1R9yP9K_EQrVnDQ", "context": "ctx:daMUL1Q1R9yP9K_EQrVnDQ", "timestamp": "2021-11-07 07:20:59", "event": "payment_aborted_by_customer"}, "object": {"status": "fail", "customer": "token:5703313655857152", "refunds": [], "auth_amount": 0, "auth_additional_amount": 0, "credit": false, "captures": [], "pos_id": "pos123", "date_modified": "2021-11-07 07:20:42", "date_expires": "2021-11-07 13:20:42", "currency": "NOK", "amount": 2900, "interchange_fee": 0, "status_code": 5006, "tid": "phkan3yvn4ex", "attachment_uri": "https://settle-core-demo.appspot.com/_ah/upload/AMmfu6a49Ta2AXX8NEgjcDCCmlfTwjiPOZ6OBK4uQ0LRwFbU-hR1JU6clKjAhFvaWjL6u6JvqLxRISX1CtXW4yp7yNqNY8-ZNVNFFhkajid8QeqnHNBIXVmtg4p7KJAVm3ElYqXPuGG9_qsxF7mf3rpmrbuYjzp2fT1iQaOwzVe--LV30upHVcxkDVy0lpJMxyKh86RKswuqlcVRM4JHpwlCW-aONlM4roY2mF14Al3fEebjTPg8n8oSlvdcwfFcbIvDTBTlpLCOOgZGGipOl5zkd4eIH8zHP6kpSwy_V5pKQqhjN1Odb6hdUuKNpJGspXrgXw3JVCz3kiRokRgy7rshbIAKLvuK2Xnn9gNR3JfTCI8AwnAcXws/ALBNUaYAAAAAYYeAs9Shob9n0EBUTaZnj-jRHCTZp1vr/", "pos_tid": "6i6tEp46pERGGi6ZbxkyV3", "permissions": null, "transaction_fee": 0, "additional_amount": 0}}';
209+
$headers = [
210+
'HTTP_AUTHORIZATION' => 'RSA-SHA256 p8bHuIN3I41lof3zEQYlEyGfj0N+uyZW2wY2xR+x7oAbfwZtRjaX8wI3QVaF23wS+d2fgJiJ0ZJqumz0rqwBcGlKy1sqhNGiA1QXfJs0o79vptex/+CGfVm7cdtCPhv2fougwHFGx6uAlYozYUpQGcIPHD4DmRFZpPpOEn7Vc9I=',
211+
'HTTP_X_SETTLE_TIMESTAMP' => '2021-11-07 07:21:00',
212+
'CONTENT_TYPE' => 'application/vnd.mcash.api.merchant.v1+json',
213+
'HTTP_X_SETTLE_CONTENT_DIGEST' => 'SHA256=qnm7VZVajBcZ+p506yfEhm7tC4hTA0q7F5YXyxd1WUA=',
214+
];
215+
$callbackUrl = 'https://daniel-zahariev.info/requestbin/settle.php';
216+
217+
expect($api_client->isValidCallback($callbackUrl, $body, $headers))->toBeTrue();
218+
});

0 commit comments

Comments
 (0)