How to do certificate authentication in Simple.OData.Client? I have X509Certificate2 which i want to use while calling the api. I use .net framework 4.6.
I did some search and I came to know it is possible to add through HttpClientHandler. But I'm not able to figure out how to do that. Below is the code i have.
void foo()
{
var clientSettings = new ODataClientSettings("");
clientSettings.OnApplyClientHandler = new Action<HttpClientHandler>(AddClientCertificate);
var client = new ODataClient(clientSettings);
}
private void AddClientCertificate(HttpClientHandler handler )
{
// I have working code to retrieve the certificate.
X509Certificate2 targetCertificate = RetrieveCertificate();
//TODO : Add the certificate to the HttpClientHandler
}
Short:
Use the ODataClientSettings.OnCreateMessageHandler and return a WebRequestHandler and setting the ClientCertificates.
I have found the solution from this github issue:
Having looked at the code again what you need to do is assign a delegate to OnCreateMessageHandler rather than OnApplyClientHandler as the underlying code creates a HttpClientHandler and you need a WebRequestHandler e.g.
var setting = new ODataClientSettings(baseAddresss, credentials)
{
OnCreateMessageHandler = {
var handler = new WebRequestHandler();
handler.ClientCertificates.Add(certificate);
return handler;
}
}
Note that if you do this, it won't call OnApplyClientHandler so you will have to also allocate any other message handlers in this delegate.
I can't easily check this out since I don't have access to a certificate secured site, but there's nothing in the code to suggest this won't work.
Hope one of the below code snippets work fine!
X509Certificate2 targetCertificate = RetrieveCertificate();
handler.ClientCertificates.Add(targetCertificate);
var filePath = rootPath + #"/App_Data/apigee.pfx";
X509Certificate2Collection certificates = new X509Certificate2Collection();
certificates.Import(filePath, "test", X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet);
httpClientHandler.ClientCertificates.AddRange(certificates);
Related
I am new to Web Services in general, and we are using .Net Framework 4.5.2, anyway I am trying to consume a web service that requires a certificate and a password.
I added the certificate gained from the providers in the project properties --> Resources --> file --> add, I also tried to use the SetCertificate() function but It seems to be a little complicated for me so I stick with loading the certificate from the properties as mentioned, however I already set all the binding setting as wanted, but somehow I am missing something, Here is my code:
string clientUrl = "some wsdl URL goes here";
BasicHttpsBinding binding = new BasicHttpsBinding
{
MaxReceivedMessageSize = Int32.MaxValue,
MaxBufferSize = Int32.MaxValue,
SendTimeout = new TimeSpan(0, 15, 0),
MessageEncoding = WSMessageEncoding.Text,
Security = {
Mode = BasicHttpsSecurityMode.Transport,
Transport = {
ClientCredentialType = HttpClientCredentialType.Certificate
}
}
};
ClientsClient testClient = new ClientsClient(binding, new EndpointAddress(new Uri(clientUrl)));
testClient.ClientCredentials.ClientCertificate.Certificate = LoadCertification();
private X509Certificate2 LoadCertification()
{
byte[] bytes = Properties.Resources.publicCert;
return new X509Certificate2(bytes, "password");
}
Note 1: The certificate extenstion is '.p12', It may be a list of certifications, if that is the case!, is it possible to pass them all?.
In the code I presented I am always getting The exception:
System.ServiceModel.ProtocolException: The 'Security' header from the namespace 'Some Http url goes here' not was understood by the recipient of the message. The message was not processed. The error usually indicates that the sender of the message has enabled a communication protocol that cannot be processed by the recipient. Verify that the client binding configuration is consistent with the service binding.
I tried to test the web service with "SOAP UI" and it worked, which made me sure that I am doing something wrong with the code, So I appreaciate any possible help that explains how to associate the certifcate in the code in the right way!.
EDIT:
in the .p12 file there are 3 certifications, which I tried to add also like this:
X509Certificate2Collection coll = LoadCertification();
int count = 0;
foreach (X509Certificate2 cert in coll)
{
testClient.ClientCredentials.ClientCertificate.Certificate = cert;
count++;// this variable is just to check the number of certificates
}
And I modified the loadCertification() method to look like this:
private X509Certificate2Collection LoadCertification()
{ string certPath = "C:/Users/ISA/Desktop/Progetti/Certificato e password/name.p12";
X509Certificate2Collection coll = new X509Certificate2Collection();
coll.Import(certPath , "password", X509KeyStorageFlags.DefaultKeySet);
return coll;
}
I'm working on several Dotnet Core APIs hosted on a Kubernettes cluster and some of the APIs do call other APIs, and that's when the exception in title is thrown.
It doesn't matter whether I edit the appsettings.json and replace all https by http -in fact people at devops team suggested me to do that- as the same exception is thrown.
This is the little piece of code I use for the http call:
int idCity = Convert.ToInt32(Utils.GetConfig().GetSection("Settings")["idCity"]);
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(Utils.GetConfig().GetSection("xxx")["xxxx"]);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
string queryString = "?startDate=" + startDate + "&endDate=" + endDate + "&idCity=" + idCity;
HttpResponseMessage response = client.GetAsync(queryString).GetAwaiter().GetResult();
if (response.IsSuccessStatusCode)
{
var resultHolidays = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return JsonConvert.DeserializeObject<JSONGeneric<HolidayDTO>>(resultHolidays);
}
else
{
return null;
}
}
I have a copy of the certificate in .crt format and also tried:
string certPath = Path.Combine(_env.ContentRootPath, _configuration.GetSection("Certificate")["certificatePath"]);
string pwd = _configuration.GetSection("Certificate")["certificatePwd"];
HttpClientHandler requestHandler = new HttpClientHandler();
requestHandler.ClientCertificates.Add(new X509Certificate2(certPath, pwd,
X509KeyStorageFlags.MachineKeySet));
using (HttpClient client = new HttpClient(requestHandler))
{
...
}
To no avail, as the same exception is thrown.
I'm not an expert on working with certificates, but I truly need to make this to work, to be able to make on api in a pod call other api, so any help will be much appreciated.
Update 1: The "weird" thing is that if I just copy the url to be requested -no matter if you use http or https- and paste it into a browser with the certificate installed it does work. If you copy and paste the http version of the url n the browser, Kubernettes (or whoever it is) does a redirection to the https version but in the end you get results. Not from .Net
I would start by disabling certificate validation in the client and see what is the behavior. You can do it like this:
var httpHandler = new HttpClientHandler {
ServerCertificateCustomValidationCallback = (m, crt, chn, e) => true
};
using var httpClient = new HttpClient(httpHandler);
// rest of the code
If the call succeeds, the next step is to adapt the certificate validation callback to check the server's certificate.
Note: in your example you're configuring a client certificate, which is useful if you host a service and want to authorize your clients based on their certificates, as described here. From the problem description I understand that what you need is the opposite: validate the server certificate in your client.
var srvCrt = new X509Certificate2(certPath, pwd);
var httpHandler = new HttpClientHandler {
ServerCertificateCustomValidationCallback = (m, crt, chn, e) => {
return crt.Thumbprint == srvCrt.Thumbprint;
}
};
using var httpClient = new HttpClient(httpHandler);
// rest of the code
I am trying to connect from a client to the service. The service is configurated to use a self signed Ssl certificate and I am trying to configurate the client with the client certificate. I am using this code:
string cacert = System.IO.File.ReadAllText("certificados/ca.crt");
string cert = System.IO.File.ReadAllText("certificados/client.crt");
string key = System.IO.File.ReadAllText("certificados/client.key");
KeyCertificatePair keypair = new KeyCertificatePair(cert, key);
SslCredentials sslCreds = new SslCredentials(cacert, keypair);
var channel = GrpcChannel.ForAddress("https://x.x.x.x:5001", new GrpcChannelOptions { Credentials = sslCreds });
var client = new Gestor.GestorClient(channel);
But I am getting the following error: using SslCredentials with non-null arguments is not supported by GrpcChannel.
I don't understand very good the message error. SslCredentials is ChannelCredentials? type, and SslCreds is Grpc.Core.SslCredentials. It can be compiled, so the type I guess it is correct.
What I would like to know it is how I can configure the client to use the self signed certificate that I have created.
Thanks.
The SslCredentials support in only available grpc-dotnet is to provide some level of compatibility with Grpc.Core in the most common use case, it doesn't expose all the functionality though. In grpc-dotnet, only SslCredentials() (parameterless which uses the default roots) is supported. If you want to provide your self-signed creds, you can certainly do that, you'll need to use a different API for configuring GrpcChannel:
See example here (creating a GrpcChannel with custom credentials).
https://github.com/grpc/grpc-dotnet/blob/dd72d6a38ab2984fd224aa8ed53686dc0153b9da/testassets/InteropTestsClient/InteropClient.cs#L170
I spend a fair bit of time googling around for solutions to this problem, and didn't find a concise answer. Here is ultimately how I was able to configure a dotnet client to use mutual SSL authentication:
MyService.MyServiceClient GetClient(){
var httpClientHandler = new HttpClientHandler();
// Validate the server certificate with the root CA
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) => {
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(new X509Certificate2("ca.crt"));
return chain.Build(cert);
};
// Pass the client certificate so the server can authenticate the client
var clientCert = X509Certificate2.CreateFromPemFile("client.crt", "client.key");
httpClientHandler.ClientCertificates.Add(clientCert);
// Create a GRPC Channel
var httpClient = new HttpClient(httpClientHandler);
var channel = GrpcChannel.ForAddress("https://localhost:8080", new GrpcChannelOptions{
HttpClient = httpClient,
});
return new MyService.MyServiceClient(channel);
}
I have an SSL certificate installed on my domain and I wanted to use it for signing with IdentityServer 4. Now I found that there is a method which would let me do that:
services.AddIdentityServer().AddSigningCredentials(certificate);
However, I cannot figure out how to actually get my certificate and pass it to the identity server.
I have tried the following:
var cert = X509Certificate.CreateFromCertFile(fileName);
services.AddIdentityServer().AddSigningCredentials(certificate);
The error that I get is it cannot convert from
'System.Security.Cryptography.X509Certificates.X509Certificate' to 'Microsoft.IdentityModel.Tokens.SIgningCredential'
Now I don't understand why it is complaining about signing credentials when one of the overrides for the method is the certificate.
I ended up resolving it like this. I'm using a shared server where I am hosting this and I could not find the file name for the certificate or the path to get it. So I ended up just opening the store and finding it that way. Not very efficient, but it will do the trick until I move it to a dedicated server and have more control.
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2 cert = null;
foreach (X509Certificate2 certificate in store.Certificates)
{
if (!string.IsNullOrWhiteSpace(certificate?.SubjectName?.Name) && certificate.SubjectName.Name.StartsWith("CN=*.mysite.com"))
{
cert = certificate;
break;
}
}
Maybe it can't convert due to issue in permissions or loading the certificate as a stream.
In my case using IdentityServer3 the below code works:
/// <summary>
/// Load the certificate that sign the Id or Jw token
/// </summary>
/// <returns></returns>
private static X509Certificate2 LoadCertificate()
{
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
return new X509Certificate2(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigMngr.GetAppSettingsValue<string>("IdSrv:SigningCertificatePath")), ConfigMngr.GetAppSettingsValue<string>("IdSrv:SigningCertificatePassword"));
}
Then in owin's startup file, I am passing it as below:
SigningCertificate = LoadCertificate(),
I know in Idsrv4 it's a different implementation than the code I posted but it should be the same abstraction, for example, you loading X509Certificate but it's deprecated so make sure to use the correct overloading to load the certificate as stream and make sure to return the correct type.
Also, This code is testable with IdSrv4:
var fileName = Path.Combine(env.WebRootPath, "FileName" );
if (!File.Exists(fileName))
{
throw new FileNotFoundException("No Signing Certificate!");
}
var cert = new X509Certificate2(fileName, "Pass" );
services.AddIdentityServer().AddSigningCredential(cert)
So instead of using
X509Certificate.CreateFromCertFile(fileName);
You can construct a new X509Certificate2 certificate like this:
var cert = new X509Certificate2(fileName, "Pass" );
And pass it to the owin middleware:
services.AddIdentityServer().AddSigningCredential(cert)
I've read at several places (like here, here or here) that it's a bad practice to dispose of the HttpClient directly after a request and it's better to dispose of it after all the request have been made, to allow reuse of the connection.
To try that out, I've created an instance of HttpClient and added to a static field in my class this way:
public class Test
{
private static X509Certificate2 _certificate;
private static HttpClient HttpClient { get; set; }
...
public Test()
{
...
if (HttpClient == null)
{
LoadCertificate();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12
| SecurityProtocolType.Ssl3;
var handler = new WebRequestHandler();
handler.ClientCertificates.Add(_certificate);
HttpClient = new HttpClient(handler, false);
}
}
private void LoadCertificate()
{
using (var store = new X509Store(StoreName.My, CertificateStoreLocation))
{
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, CertificateFriendlyName, true);
if (certificates.Count != 1)
throw new ArgumentException(
$"Cannot find a valid certificate with name {CertificateFriendlyName} in {CertificateStoreLocation}");
_certificate = certificates[0];
store.Close();
}
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
}
}
I'm then using my instance to call a web service through this command:
var result = await HttpClient.PostAsJsonAsync(completeUri, request);
The first time I'm running the code, everything works fine and I get a response correctly, but then, all the following time I get an unauthorized from the server telling me that I didn't use a client certificate.
It's like if for the following calls, the WebRequestHandler wasn't took into consideration.
Your fix should look like this:
handler.PreAuthenticate = true;
Once you establish a connection to a service, you can re-use it to communicate with it using different clients with different auth information. That means, the service needs to know which client sent a request each time, otherwise it could be a security breach - e.g. executing a request under last connected client. It depends on your authentication mechanism, but basically WebRequestHandler sets flag IsAuthenticated after the first request, and stops sending the auth information on next requests. The PreAuthenticate options forces to send the auth info on every request.