Can't create a matching signature with response
jesseSalonen opened this issue · 2 comments
Hello!
I'm trying to integrate Paytrail into my Laravel API, but have now ran into problems.
I have been testing the API for a while now and can't get the response's signature header to match with my own calculated one.
I have used the documentation HMAC example with php to test the endpoint for creating a new payment.
My own calculated HMAC is the same as in the example with same header values, but I get the "signature mismatch" -error in response. I can' find the source for this problem.
Is there something wrong with my code, or am I missing something?
Thank you for your help!
The code:
private $testAccount;
private $testSecret;
private $endpoint;
function __construct()
{
$this->testAccount = 375917;
$this->testSecret = 'SAIPPUAKAUPPIAS';
$this->endpoint = 'https://services.paytrail.com/';
}
public function startPaytrailPayment(Request $request): \Illuminate\Http\JsonResponse {
try {
$method = 'POST';
$nonce = '564635208570151';
$timestamp = '2018-07-06T10:01:31.904Z'; // ISO 8601 date time
$params = [
'stamp' => 'unique-identifier-for-merchant',
'reference' => '3759170',
'amount' => 1525,
'currency' => 'EUR',
'language' => 'FI',
'items' => [
[
'unitPrice' => 1525,
'units' => 1,
'vatPercentage' => 24,
'productCode' => '#1234',
'deliveryDate' => '2018-09-01'
]
],
'customer' => [
'email' => 'test.customer@example.com'
],
'redirectUrls' => [
'success' => 'https://ecom.example.com/cart/success',
'cancel' => 'https://ecom.example.com/cart/cancel'
]
];
$headers = $this->getPaytrailHeaders($method, $nonce, $timestamp);
$body = $this->getPaytrailBody($method, $params);
$headers['signature'] = $this->calculateHmac($headers, $body);
$response = Http::withHeaders($headers)->post($this->endpoint.'payments', ['body' => $body]);
if ($response->successful()) {
if ($this->isResponseHmacValid($method, $response, $nonce, $timestamp)) {
return response()->json(
$response->json(),
Response::HTTP_OK
);
} else {
return response()->json([
"message" => "Paytrail response HMAC signature mismatch!"
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
} else {
if ($response->serverError()) {
return response()->json([
"message" => $response->json()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
return response()->json([
"message" => $response->json()
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $ex) {
return response()->json([
"message" => "Failed to start payment process."
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Calculate Checkout HMAC
*
* @param string $secret Merchant shared secret key
* @param array[string]string $params HTTP headers or query string
* @param string $body HTTP request body, empty string for GET requests
* @return string SHA-256 HMAC
*/
private function calculateHmac($params, $body = '')
{
// Keep only checkout- params, more relevant for response validation. Filter query
// string parameters the same way - the signature includes only checkout- values.
$includedKeys = array_filter(array_keys($params), function ($key) {
return preg_match('/^checkout-/', $key);
});
// Keys must be sorted alphabetically
sort($includedKeys, SORT_STRING);
$hmacPayload =
array_map(
function ($key) use ($params) {
return join(':', [ $key, $params[$key] ]);
},
$includedKeys
);
array_push($hmacPayload, $body);
$hmac = hash_hmac('sha256', join("\n", $hmacPayload), $this->testSecret);
return $hmac;
}
private function getPaytrailHeaders($method, $nonce, $timestamp) {
return [
'checkout-account' => $this->testAccount,
'checkout-algorithm' => 'sha256',
'checkout-method' => $method,
'checkout-nonce' => $nonce,
'checkout-timestamp' => $timestamp,
'content-type' => 'application/json; charset=utf-8'
];
}
private function getPaytrailBody($method, array $params) {
if ($method === "GET") {
return '';
} else if (!empty($params)) {
return json_encode(
$params,
JSON_UNESCAPED_SLASHES
);
} else {
return '';
}
}
private function isResponseHmacValid($method, \Illuminate\Http\Client\Response $response, $nonce, $timestamp) {
$headers = $response->headers();
if ($method === "GET") {
$responseBody = "";
} else {
$responseBody = $response->body();
}
// Flatten Guzzle response headers
$responseHeaders = array_column(array_map(function ($key, $value) {
return [ $key, $value[0] ];
}, array_keys($headers), array_values($headers)), 1, 0);
if ($method === "GET") {
if (!isset($responseHeaders["checkout-nonce"])) {
$responseHeaders["checkout-nonce"] = $nonce;
}
if (!isset($responseHeaders["checkout-timestamp"])) {
$responseHeaders["checkout-timestamp"] = $timestamp;
}
}
$responseHmac = $this->calculateHmac($responseHeaders, $responseBody);
if ($responseHmac !== $response->header('signature')[0]) {
return false;
} else {
return true;
}
}
The payload for HMAC function:
checkout-account:375917
checkout-algorithm:sha256
checkout-method:POST
checkout-nonce:564635208570151
checkout-timestamp:2018-07-06T10:01:31.904Z
{"stamp":"unique-identifier-for-merchant","reference":"3759170","amount":1525,"currency":"EUR","language":"FI","items":[{"unitPrice":1525,"units":1,"vatPercentage":24,"productCode":"#1234","deliveryDate":"2018-09-01"}],"customer":{"email":"test.customer@example.com"},"redirectUrls":{"success":"https://ecom.example.com/cart/success","cancel":"https://ecom.example.com/cart/cancel"}}
HMAC: 3708f6497ae7cc55a2e6009fc90aa10c3ad0ef125260ee91b19168750f6d74f6
Here are all the request headers:
array (
'checkout-account' => 375917,
'checkout-algorithm' => 'sha256',
'checkout-method' => 'POST',
'checkout-nonce' => '564635208570151',
'checkout-timestamp' => '2018-07-06T10:01:31.904Z',
'content-type' => 'application/json; charset=utf-8',
'signature' => '3708f6497ae7cc55a2e6009fc90aa10c3ad0ef125260ee91b19168750f6d74f6',
)
The response:
{"status":"error","message":"Signature mismatch"}
@jesseSalonen Were you aware that we have made PHP-SDK available as well?
https://github.com/paytrail/paytrail-php-sdk
It might help you get up and running faster.
@loueranta-paytrail I weren't aware of that, thank you!
Now everything seems to be working correctly!