Donavan John Wallis
Donavan John Wallis

Reputation: 69

Payfast API 401 error C#. Merchant authorization failed

I am currently trying to Ping the Payfast API to get basic authentication rate.

I am stuck at getting the signature correct I think.

To generate the signature I am sorting all variables in alphabetical order as seen here and URLencoding:

string signature = HttpUtility.UrlEncode($"merchant-id={merchantID}&passphrase=​{passphrase}&timestamp={timestamp}&version={version}");

I am then generating the MD5 hash string as seen here

using (var md5Hash = MD5.Create())
{
    // Byte array representation of source string
    var sourceBytes = Encoding.UTF8.GetBytes(signature);
    
    // Generate hash value(Byte Array) for input data
    var hashBytes = md5Hash.ComputeHash(sourceBytes);

    // Convert hash byte array to string
    var hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty);

    // Output the MD5 hash
    Console.WriteLine(signature + " is: " + hash);
    string hashlower = hash.ToLower()
}

I then add the signature to the header

request.AddHeader("merchant-id", merchantID);
request.AddHeader("version", version);
request.AddHeader("signature", hashlower);
request.AddHeader("timestamp", timestamp);

This seems correct when viewing an example in their postman collection https://documenter.getpostman.com/view/10608852/TVCmSQZu

Could anyone pick anything up that I am doing wrong in the signature generation

ull code

string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH\\:mm\\:ss", CultureInfo.InvariantCulture);
string signature = HttpUtility.UrlEncode($"merchant-id={merchantID}&passphrase=​{passphrase}&timestamp={timestamp}&version={version}");

using (var md5Hash = MD5.Create())
{
    // Byte array representation of source string
    var sourceBytes = Encoding.UTF8.GetBytes(signature);

    // Generate hash value(Byte Array) for input data
    var hashBytes = md5Hash.ComputeHash(sourceBytes);

    // Convert hash byte array to string
    var hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty);

    // Output the MD5 hash
    Console.WriteLine(signature + " is: " + hash);
    string hashlower = hash.ToLower();

    var client = new RestClient("https://api.payfast.co.za/ping");
    client.Timeout = -1;

    var request = new RestRequest(Method.GET);
    request.AddHeader("merchant-id", merchantID);
    request.AddHeader("version", version);
    request.AddHeader("signature", hashlower);
    request.AddHeader("timestamp", timestamp);
              
    IRestResponse response = client.Execute(request);

    Console.WriteLine(response.Content);
}

Upvotes: 1

Views: 446

Answers (2)

Rinus van Heerden
Rinus van Heerden

Reputation: 245

Here is a working implementation, where "data" contains the passphrase.

    private string GenerateSignature(Dictionary<string, string> data)
{
    var orderedData = new SortedList<string, string>(data);
    
    var getString = 
        string.Join("&", orderedData.Select(item => $"{item.Key}={Uri.EscapeDataString(item.Value)}"));

    using var md5 = MD5.Create();
    var hashedBytes = md5.ComputeHash(Encoding.ASCII.GetBytes(getString));
    
    var sb = new StringBuilder(hashedBytes.Length * 2);
    foreach (byte b in hashedBytes)
    {
        sb.Append(b.ToString("x2"));
    }
    return sb.ToString();
}

Upvotes: 0

EntityBox
EntityBox

Reputation: 1

I took the API Documentation and converted line by line matching everything, after copying, and running their Node.js and Python sample code.

Two things came out in my findings.

URL Encoding.

When you URL Encode in c#, eg your date:
2023-02-20T20:00:00 becomes 2023-02-20T20%3a00%3a00

PayFast from Python and Node.js Encode as Uppercase, so:
2023-02-20T20:00:00 becomes 2023-02-20T20%3A00%3A00

Notice the 'A' in the latter instance, ensure your URL encoding is correct.
Use Uri.EscapeDataString([string])

Hash Encoding:

You are assuming that the Encoding is using UTF-8 in c# is the same the Crypto hashing from Node.js and Python, which is in fact ASCII after spending time going through all the variations. Update the following:

var sourceBytes = Encoding.UTF8.GetBytes(signature);

to the following:

var sourceBytes = Encoding.ASCII.GetBytes(signature);

Hex your Hash!

You also need to Hex your hash before you return it

var sb = new StringBuilder(hash.Length * 2);
foreach (byte b in hash)
{
     sb.Append(b.ToString("x2")); // x2 is lowercase
}
signature = sb.ToString();

Wrap everything into a nice global method with a SortedList<string,string> as input, iterate through the values, string them with a StringBuilder, MD5 Hash it, and return your signature.

(Yes, a SortedList<TKey,TValue> automatically sorts the list as you enter the values on TKey string, which is a requirement in the API Documentation.)

Upvotes: 0

Related Questions