paytrail/api-documentation

More detailed HMAC examples to use in other languages

niklaswulff opened this issue · 11 comments

I would like a concise example of calculating HMAC signature, with step by step instructions and partial results.

I'm using C#, so the examples are not 100% usable to me. I would like to see more detailed steps to be able to figure out where the calculation I perform isn't producing the same HMAC as in the example

For instance:
return crypto.createHmac('sha256', secret).update(hmacPayload).digest('hex');

I would like to find the exact C# equivalent, so using a very simple hmacPayload, what would the output be?

const hmacPayload = 'asdf';
const result = crypto.createHmac('sha256', secret).update(hmacPayload);
result = '????'

and what does the "digest(hex)" do?

result.digest('hex') = '????'

Just got it work with C# today

var requestHeaders = new Dictionary<string, string>() {
{ "checkout-account", "375917" },
{ "checkout-algorithm", "sha256" },
{ "checkout-method", "POST" },
{ "checkout-nonce", "564635208570146" },
{ "checkout-timestamp", "2022-04-08T10:01:31.904Z" }
};

var secretKey = "SAIPPUAKAUPPIAS";
string getHMACPayLoad(Dictionary<string, string> headers, string body) {
var headerPart = "";
foreach (var item in (headers.OrderBy(x => x.Key).ToDictionary(y => y.Key, y => y.Value))) {
headerPart += $"{item.Key}:{item.Value}\n";
}
//Minimize body json
var bodyPart = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(body));
return headerPart + bodyPart;
}

static string getHMac256String(string secretKey, string message) {
var encoding = new UTF8Encoding();
var hash = new HMACSHA256(encoding.GetBytes(secretKey));
var hmac = hash.ComputeHash(encoding.GetBytes(message));
return BitConverter.ToString(hmac).Replace("-", "").ToLower();
}

//Get Signature For Header
var payLoad = getHMACPayLoad(requestHeaders, requestBody);
Console.WriteLine(getHMac256String(secretKey, payLoad));

Great, thanks @Kepe123 !

The Json handling of body content was my weak part, the rest I had exactly as you. I'm still unsure on how to properly unit test this, as the examples in the documentation is a bit unclear on the exact body content. Do you have any good data that I can use to verify?

I think the specification should also define the collation order for "alphabetic order" needed for the checksum calculation. I would assume the spec actually means ASCII ordering but it should be defined explicitly.

Great, thanks @Kepe123 !

The Json handling of body content was my weak part, the rest I had exactly as you. I'm still unsure on how to properly unit test this, as the examples in the documentation is a bit unclear on the exact body content. Do you have any good data that I can use to verify?

I first tested with an empty body and then with only the "stamp" field in body json until the error in response was something else than "signature mismatch". Then used the same json as in their examples section to successfully fetch the full results (Stamp propably should change between requests or there was "stamp already used" etc. error).

var requestBody = @"{
""stamp"": ""29858472953"",
""reference"": ""9187445"",
""amount"": 1590,
""currency"": ""EUR"",
""language"": ""FI"",
""items"": [
{
""unitPrice"": 1590,
""units"": 1,
""vatPercentage"": 24,
""productCode"": ""#927502759"",
""deliveryDate"": ""2018-03-07"",
""description"": ""Cat ladder"",
""category"": ""Pet supplies""
}
],
""customer"": {
""email"": ""erja.esimerkki@example.org"",
""firstName"": ""Erja"",
""lastName"": ""Esimerkki"",
""phone"": ""+358501234567"",
""vatId"": ""FI12345671""
},
""deliveryAddress"": {
""streetAddress"": ""Hämeenkatu 6 B"",
""postalCode"": ""33100"",
""city"": ""Tampere"",
""county"": ""Pirkanmaa"",
""country"": ""FI""
},
""invoicingAddress"": {
""streetAddress"": ""Testikatu 1"",
""postalCode"": ""00510"",
""city"": ""Helsinki"",
""county"": ""Uusimaa"",
""country"": ""FI""
},
""redirectUrls"": {
""success"": ""https://ecom.example.org/success"",
""cancel"": ""https://ecom.example.org/cancel""
},
""callbackUrls"": {
""success"": ""https://ecom.example.org/success"",
""cancel"": ""https://ecom.example.org/cancel""
}
}";

Sounds like a smart approach, I will use the same. :-)

I haven't seen any detailed error messages though, only 401, gotta look harder.

@Kepe123 , I'm really not getting the signature correct...

With the headers that you sent on 28/4 8:17 PM, and an empty body - what HMAC should I expect? It's so frustrating to simply get "signature mismatch", and not having better test data.

@Kepe123 I have now finally managed to recreate the example HMACS.

But I get "signature mismatch" when calling Paytrail, even when I'm using their example payloads, which is kind of strange. This is my C# code for doing the HTTP Post, is there something wrong here?

    var httpRequestMessage = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri(CreatePaymentUrl),
        Headers = {
            { HttpRequestHeader.ContentType.ToString(), "application/json; charset=utf-8" },
            { HttpRequestHeader.Accept.ToString(), "application/json" },
            { "signature", signature },
        },
        Content = new StringContent(requestBody)
    };

     createPaymentNewPayTrail = new CreatePaymentNewPayTrail
        {
            Secret = "SAIPPUAKAUPPIAS",
            AccountId = "375917",
            Nonce = "564635208570151",
            TimeStamp = "2018-07-06T10:01:31.904Z",
    // Sample data from Paytrail...
        };

        foreach (var header in createPaymentNewPayTrail.Headers)
        {
            httpRequestMessage.Headers.Add(header.Key, header.Value);
        }

       var response = await client.SendAsync(httpRequestMessage);

@niklaswulff
Did you ever get this sorted out? I'm having trouble as well.
I think I have the HMAC created finally as I am not getting the signature mismatch error anymore, but now I'm getting a 'bad request' error when doing a API call through the httpclient.

{ "signature", signature },

How do you compute the signature? That part was missing from your example. Note that the header names must start with literal string checkout- to be included in the signature.

I was able to get it working with the code above for requests that have an empty body, i.e. getting payment methods, but for this to work the following line needs to be changed

var bodyPart = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(body));

as otherwise the bodyPart will contain "null" instead of "" which creates an invalid signature.

Great to see that a solution was found. Adding examples to more languages is a thing we are currently looking at and hope to provide more examples in the future.