PAYONE API HMAC authentication in AL

This is one of the more specific posts about API authentication in Business Central. But probably someone could use the snippets for his own implementation. Right now we’re implementing the payment API of PAYONE for one of our customers. This provider needs the API request headers and additional information to be hashed using HMAC256 and send with every request.

So, actually this a specific HowTo on the PAYONE API authentication.

This code here has not yet been optimized, generalized and simplified! It is more or less hardcoded and the intention is just to get you an idea how to approach such an authentication process.

Authentication steps

  1. Create a string-to-hash, consisting of several HTTP headers
  2. Calculate the hash using the algorithm HMAC-SHA256 with your API Secret
  3. Send the actual request, including the headers, the hash and your API Key

Create the String

With this function we create the string to be hashed. It could consist of every information you want.
The lines are separated using a line feed.

local procedure CreateStringToHash(pPSPId: Text; pUriResource: Text; ApiIdentifier: Text; pRestMethods: Enum "Http Request Type"; pFormattedDateTime: Text): Text
var
    stringToHash: Text;
    contentType: Text;
    endpointURL: Text;
    requestMethod: Text;
    LF: char;
begin
    LF := 10;
    requestMethod := Format(pRestMethods);

    if requestMethod = 'POST' then
       contentType := 'application/json';

    if CopyStr(pUriResource, 1, 1) <> '/' then
       pUriResource := '/' + pUriResource;

    endpointURL := '/v2/' + pPSPId + pUriResource;

    if ApiIdentifier <> '' then
       endpointURL += '/' + ApiIdentifier;

    stringToHash := requestMethod + LF + contentType + LF + pFormattedDateTime + LF + endpointURL + LF;

    exit(stringToHash);
end;

Hashing the String

With this method we hash the string using our API secret using the standard Cryptography Management codeunit.

local procedure CreateAuthenticationHash(StringToHash: Text; ApiSecret: Text): Text
var
   CryptoMgt: Codeunit "Cryptography Management";
   HashAlgorithmType: Option HMACMD5,HMACSHA1,HMACSHA256,HMACSHA384,HMACSHA512;
begin
   exit(CryptoMgt.GenerateHashAsBase64String(StringToHash, ApiSecret, HashAlgorithmType::HMACSHA256));
end;

Request

Using these procedures we could now create an API request:

PSPId := '';
ApiKey := '';
ApiSecret := '';

lDateTime := CurrentDateTime;
FormattedDatetime := CreateFormattedDateTime(lDateTime);

StringToHash := CreateStringToHash(PSPId, '/hostedcheckouts', Enum::"Http Request Type"::POST, FormattedDatetime);
AuthorizationHash := CreateAuthenticationHash(StringToHash, ApiSecret);
AuthorizationHeader := StrSubstNo('GCS v1HMAC:%1:%2', lApiKey, AuthorizationHash);

HttpRequestMessage.Method := 'POST';
HttpRequestMessage.SetRequestUri(StrSubstNo('https://payment.preprod.payone.com/v2/%1/hostedcheckouts', PSPId);

HttpRequestMessage.GetHeaders(HttpHeaders);
HttpHeaders.Add('Date', FormattedDatetime);
HttpHeaders.Add('Authorization', AuthorizationHeader);

if HttpRequestMessage.Method = 'POST' then begin

   HttpContent.WriteFrom('{ "key": "value" }');

   HttpContent.GetHeaders(HttpHeaders);
   if HttpHeaders.Contains('Content-Type') then
      HttpHeaders.Remove('Content-Type');
   
   HttpHeaders.Add('Content-Type', 'application/json');
   HttpRequestMessage.Content := HttpContent;
end;

HttpClient.Send(HttpRequestMessage, HttpResponseMessage);

Helper: Formatting date

This API needs a specific format in the “date” header, so we create a helper function to do that. As we are situated in the CET timezone and don’t want to mess around with day and month translations, we do some magic here as well. The result should look like:

Wed, 02 Mar 2024 11:15:51 GMT
local procedure CreateFormattedDateTime(pDatetime: DateTime): Text
var
   FormattedDatetime: Text;
   LanguageID: Integer;
begin
   LanguageID := GlobalLanguage;
   GLOBALLANGUAGE := 1033;
   FormattedDatetime := FORMAT(pDateTime - 3600000, 0, '<Weekday Text,3>, <Day,2> <Month Text,3> <Year4> <Hour,2>:<Minute,2>:<Second,2> GMT');
   GLOBALLANGUAGE := LanguageID;

   exit(FormattedDatetime);
end;

As already mentioned this could now be wrapped, simplified and used for all API calls.

... is a technical consultant and developer at Comsol Unternehmenslösungen AG in Kronberg/Taunus. Major tasks are the architecture and implementation of complex, usually cross-system applications in and around Microsoft Dynamics 365 Business Central.

Leave a Reply

Your email address will not be published. Required fields are marked *