I'm trying to follow this get started guide to make an access token request. It provides a bash script as an example of how to do that. Another requirement is to make this call from Azure Function, so I've created an HTTP triggered Azure Function project in Visual Studio 2019.
It consists of 4 parts:
There are two certificates provided as key pairs in .cer
and .key
files. One is to authenticate a request, another to create a signature with. I've combined the public and private keys into a .pfx
container with openssl
command like so:
openssl pkcs12 -export -in my.cer -inkey my.key -out mycert.pfx
and loaded them using:
public static X509Certificate2 GetCertificate(this ExecutionContext ctx, string certFileName)
{
return new X509Certificate2(Path.GetFullPath(Path.Combine(ctx.FunctionAppDirectory, @$"Certs\{certFileName}")), "mypass");
}
I'm using FormUrlEncodedContent
class for my payload, so digest is computed like so:
public static string ComputeSHA256HashAsBase64String(this string stringToHash)
{
using (var hash = SHA256.Create())
{
Byte[] result = hash.ComputeHash(Encoding.UTF8.GetBytes(stringToHash));
return Convert.ToBase64String(result);
}
}
public static async Task<string> DigestValue(this FormUrlEncodedContent content)
{
var payload = await content.ReadAsStringAsync();
return "SHA-256=" + payload.ComputeSHA256HashAsBase64String();
}
var currentDate = DateTime.Now.ToUniversalTime().ToString("r");
var signingString =
@$"(request-target): {IgnAccountApi.HttpMethodStr} {IgnAccountApi.AccessTokenPath}
date: {currentDate}
digest: {digest}";
var signature = cert.SignData(signingString);
public static string SignData(this X509Certificate2 cert, string stringToSign)
{
using (var hash = SHA256.Create())
{
var dataToSign = Encoding.UTF8.GetBytes(stringToSign);
Byte[] hashToSign = hash.ComputeHash(dataToSign);
var signedData = cert.GetRSAPrivateKey().SignData(hashToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signedData);
}
}
The curl
request I'm trying to reproduce (from the mentioned bash script):
curl -v -i -X POST "${httpHost}${reqPath}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H "Digest: ${digest}" \
-H "Date: ${reqDate}" \
-H "authorization: Signature keyId=\"$keyId\",algorithm=\"rsa-sha256\",headers=\"(request-target) date digest\",signature=\"$signature\"" \
-d "${payload}" \
--cert "${certPath}example_client_tls.cer" \
--key "${certPath}example_client_tls.key"
I'm using HttpClientHandler
to add tls.pfx
I've created using openssl
to HttpClient
and make an authorized request like so:
using (var cert = ctx.GetCertificate("tls.pfx"))
{
// https://stackoverflow.com/questions/40014047/add-client-certificate-to-net-core-httpclient
var _clientHandler = new HttpClientHandler();
_clientHandler.ClientCertificates.Add(cert);
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
_clientHandler.SslProtocols = SslProtocols.Tls12;
var dataToSend = new Dictionary<string, string>
{
{ "grant_type","client_credentials" },
};
using (var content = new FormUrlEncodedContent(dataToSend))
using (var _client = new HttpClient(_clientHandler))
{
var request = new HttpRequestMessage(HttpMethod.Post, $"{HttpHost}{AccessTokenPath}");
request.Content = content;
request.AddHeaders(ctx.GetCertificate("sign.pfx"), await content.DigestValue());
using (HttpResponseMessage response = await _client.SendAsync(request))
{
// TODO: process the response
}
}
}
To not left anything out, here's AddHeaders
extension method:
public static void AddHeaders(this HttpRequestMessage request, X509Certificate2 cert, string digest)
{
var currentDate = DateTime.Now.ToUniversalTime().ToString("r");
var signingString =
@$"(request-target): {IgnAccountApi.HttpMethodStr} {IgnAccountApi.AccessTokenPath}
date: {currentDate}
digest: {digest}";
var signature = cert.SignData(signingString);
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Digest", digest);
request.Headers.Add("Date", currentDate);
request.Headers.Add("authorization", $"Signature keyId=\"{IgnAccountApi.ClienId}\",algorithm=\"rsa-sha256\",headers=\"(request-target) date digest\",signature=\"{signature}\"");
}
The code above is generation the following request:
POST https://api.sandbox.ing.com/oauth2/token HTTP/1.1
Host: api.sandbox.ing.com
Accept: application/json
Digest: SHA-256=w0mymuL8aCrbJmmabs1pytZhon8lQucTuJMUtuKr+uw=
Date: Tue, 21 Apr 2020 09:02:56 GMT
Authorization: Signature keyId="e77d776b-90af-4684-bebc-521e5b2614dd",algorithm="rsa-sha256",headers="(request-target) date digest",signature="BaQgDXTsGBcZfZa+9oeaQhkv7bQwbMw92h4Dwp/EexJnjScScqVMYFwRSskkN1fYfu/1lDE+/K27qEJD9cq8i68C6u29I9wsUWlRtAiHu10d/hzTcZkfWLpoSKSo4mg016I//K/4scdnwf0fcsNgDOXYaoe9/KscltreXn6UQuYuwP98uZDTP3j/V7k34R5VIMPaUm1MSvE3H5opGNbLqpBjK8IenKUHjF0B9aqCzGB30eA7Y+fL025wRko6mGY2f+u4w3mi1RJzTb72Cw3SPejaa5s65sYIAus14g975RPBI4B7A2o/vsZ39Np1yJNvCW1tbZaTGAF4IJUfXQashw=="
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
grant_type=client_credentials
The response I'm getting is rather disappointing:
HTTP/1.1 400
Date: Sat, 18 Apr 2020 06:17:20 GMT
Content-Type: application/json
Content-Length: 98
Connection: keep-alive
X-Frame-Options: deny
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31622400; includeSubDomains
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Security-Policy: default-src 'self'; prefetch-src 'none'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; connect-src 'self'; style-src 'self' 'unsafe-inline' data:; img-src https: data:; script-src 'self' data: 'unsafe-inline' 'unsafe-eval'
{
"message" : "InputValidation failed: Field 'X-ENV-SSL_CLIENT_CERTIFICATE' was not provided."
}
To compare requests made from curl, here is the request generated using provided bash script (unfortunately fiddler isn't seeing this one, so it's just copy/paste form the console window):
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fffe7479580)
> POST /oauth2/token HTTP/2
> Host: api.sandbox.ing.com
> User-Agent: curl/7.58.0
> Accept: application/json
> Content-Type: application/x-www-form-urlencoded
> Digest: SHA-256=w0mymuL8aCrbJmmabs1pytZhon8lQucTuJMUtuKr+uw=
> Date: Fri, 17 Apr 2020 14:56:12 GMT
> authorization: Signature keyId="e77d776b-90af-4684-bebc-521e5b2614dd",algorithm="rsa-sha256",headers="(request-target) date digest",signature="KynniiPdkPoVWu5YqXl+YMXQlmYa5C7Wp5ih+/HQtiSlgcNXZlHiRoKmhrwvaDo/0qXHGexJxxrrcgWYnJz3DusgUpr30Xg6DbcD8VlN6kYvk3DUez2q2+CmYo93ulVdz9W7+V0xQdEr6jLHZc/TLcpMUQly11ADiiBPUMhGd4VfN4XTwCcsoq/mPQ5tqVM+3hln5r85jDzf2sFjt/Is4do8WCwZjfdoNBdgtS3k73oBH1kS/foRxzS5ke6fxFaN2Al1o9dkDMhrOV7TQl0wOCIbmkgBRdQXA4Rq83HO3t3R65x+RVHafRfRT6o8bNTIqgy51aKVzqhdBvUQC6Dwkg=="
> Content-Length: 29
What am I doing wrong? Is it possible to authenticate a request with certificates using HttpClient
class? Please help!
UPDATE 1: Added HTTP request generated by my code and HTTP request generated by the aforementioned bash script provided by (get started guide).
I figured it out.
First of all, it was Fiddler that somehow broke the request and removed the certificate from it.
When I got Fiddler out of my way, I discovered, that my method to generate the signature is wrong and data I'm generating it from is also incorrect.
The correct method to generate the signature is the following:
public static string SignData(this X509Certificate2 cert, string stringToSign)
{
var dataToSign = Encoding.UTF8.GetBytes(stringToSign);
var signedData = cert.GetRSAPrivateKey().SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var base64Signature = Convert.ToBase64String(signedData);
return base64Signature;
}
The correct data to sign is the following:
var stringToSign = $"(request-target): {httpMethod} {httpPath}\ndate: {currentDate}\ndigest: {digest}";
Hope this will help someone trying to integrate with the ING APIs. Thank you, everyone, for help.