I'm trying to implement basic authentication on a Web API server and a client that calls the API.
This is how I'm configuring my HttpClient object in my client code (note how I'm setting the credentials).
services.AddHttpClient<TrackAndTraceClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(settings.BaseUrl);
httpClient.Timeout = TimeSpan.FromMinutes(5);
})
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>
{
return new HttpClientHandler()
{
Credentials = new NetworkCredential(settings.Username, settings.Password),
};
});
However, this results in no Authorization header being received on the server end.
My question is, what does setting the credentials this way actually do? Where are those credentials placed?
And is it possible to write my server so that it handles credentials configured this way?
You can add Authorization header to your client-side code as follows:
services.AddHttpClient<TrackAndTraceClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(settings.BaseUrl);
httpClient.Timeout = TimeSpan.FromMinutes(5);
var authenticationBytes = Encoding.ASCII.GetBytes($"{settings.Username}:{settings.Password}");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authenticationBytes));
});
You can use AuthenticationHandler to check Authorization header on the server side. Here is an example of how to implement Basic Authentication using AuthenticationHandler.
The PreAuthenticate should send the authentication header.
services.AddHttpClient<TrackAndTraceClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(settings.BaseUrl);
httpClient.Timeout = TimeSpan.FromMinutes(5);
})
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>
{
return new HttpClientHandler()
{
PreAuthenticate = true,
Credentials = new NetworkCredential(settings.Username,
settings.Password)
};
});
If that does not work, it may be due to the Credentials property in the HttpClientHandler class having the[UnsupportedOSPlatform("browser")] attribute (as of .NET 5) -- note so does the PreAuthenticate property. In addition, the HttpClientHandler class also contains the following definition:
The default message handler used by HttpClient in .NET Framework and .NET Core 2.0 and earlier.
I see that your post has the tag .NET 6, so that might could also be a factor on why it's not sending. If I was you, I would use a middleware to rece9ve the authentication header from the request and use the solution #Burhan Savci posted for sending it.
Assuming your backend is a .NET Core API similar:
public class BasicAuthMiddleware
{
private readonly RequestDelegate _next;
public BasicAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
string authHeader =
httpContext.Request.Headers["Authorization"];
if (authHeader != null)
{
string auth = authHeader.Split(new char[] { ' ' })[1];
Encoding encoding = Encoding.GetEncoding("UTF-8");
var usernameAndPassword =
encoding.GetString(Convert.FromBase64String(auth));
string username = usernameAndPassword
.Split(new char[] { ':' })[0];
string password = usernameAndPassword
.Split(new char[] { ':' })[1];
if (username == "abc" && password == "123")
{
await _next(httpContext);
}
else
{
httpContext.Response.StatusCode = 401;
return;
}
}
else
{
httpContext.Response.StatusCode = 401;
return;
}
}
}
And then in the configure method of your Startup class:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<BasicAuthMiddleware>();
}
Related
ASP.Net Core Web API Call Thirds party API fails intermittently.
The following exception raises intermittently when load test with postman.
"Call failed with status code 500 (Internal Server Error): POST https://sample.com/apiendpoint."
I tried the Named/Typed with HttpClient/IHttpClientFactory approach and the problem continues.
How to make sure it uses connection pooling and not create new on one.
what is the right value for SetHandlerLifetime to keep the connection in the pool for future call to use.
services.AddHttpClient<IRestService, RestServiceOne>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes
.AddPolicyHandler(GetRetryPolicy());
The following code is in RestServiceOne.cs
public class RestServiceOne : IRestService
{
private readonly IHttpClientFactory _httpClientFactory;
public RestServiceOne(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HttpResponseMessage> GetDataAsync(string destinationUrl, string user,
string password, string requestXml, string orderNumber, CancellationToken cancellationToken)
{
var endpoint = $"{destinationUrl}";
var authToken = Encoding.ASCII.GetBytes($"{user}:{password}");
var data = new StringContent(requestXml, Encoding.UTF8, "text/xml");
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Post,
endpoint)
{
Headers =
{
{ "Accept", "application/vnd.github.v3+json" },
{ "User-Agent", "HttpRequestsConsoleSample" }
}
};
httpRequestMessage.Content = data;
var httpClient = _httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(authToken));
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
return httpResponseMessage;
}
}
I also tried HttpClient injection given in Microsoft type example.
public class RestService
{
private readonly HttpClient _httpClient;
public RestService(HttpClient httpClient)
{
_httpClient = httpClient;
try
{
_httpClient.BaseAddress = new Uri("https://testxx.com/test");
// GitHub API versioning
_httpClient.DefaultRequestHeaders.Add("Accept",
"application/vnd.github.v3+json");
// GitHub requires a user-agent
_httpClient.DefaultRequestHeaders.Add("User-Agent",
"HttpClientFactory-Sample");
}
catch(Exception ex)
{
}
}
public async Task<HttpResponseMessage> GetDataAsync(string destinationUrl, string user,
string password, string requestXml, string orderNumber, CancellationToken cancellationToken)
{
var endpoint = $"{destinationUrl}";
var authToken = Encoding.ASCII.GetBytes($"{user}:{password}");
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(authToken));
var data = new StringContent(requestXml, Encoding.UTF8, "text/xml");
try
{
var response = await _httpClient.PostAsync(endpoint, data);
return response;
}
catch(Exception ex)
{
throw;
}
}
}
I tried with Flurl directly in service layer
var endpoint = $"{destinationUrl}";
var authToken = Encoding.ASCII.GetBytes($"{user}:{password}");
var data = new StringContent(requestXml, Encoding.UTF8, "text/xml");
try
{
var response = await endpoint
.WithHeader("Content-Type", "text/xml")
.WithHeader("app-bu-id", "SANIDERMMEDICAL")
.WithHeader("Authorization", new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authToken)))
.PostAsync(data);
The above all 3 approach failed.
what is right value for .SetHandlerLifetime() to keep the connection in the pool and avoid creating new.
I saw some other example using the following approach but how to use this with IHttpClientFactory / Flurl.
var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
};
var client = new HttpClient(socketsHandler);
How can I ensure it use connection pooling and avoid the 500 error when calling 3rd party API from Azure.
I found solution when I use httpclient as given below.
private static readonly HttpClient httpClient = new HttpClient(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(60),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(20),
MaxConnectionsPerServer = 10
});
This article helped me
Tried couple of value for SocketsHttpHandler but finally choose this since no error.
We are working on an idetityserver4 (A SPA application in angular) that will run on a standalone server and will comunicate with an API(asp.net API) that is on another server, the patern we are trying to implement is BFF (backend for front end) and if we didn't misunderstand the concept badly, our ID4 will act as the gateway to the API, firstly we log to the ID4 with the ClientID and secret so that we get a token and generate the session cookies, afterward we forward everything to the API to complete the local login.
Everything works fine till here, we get the response we want, token gets set to acces the API and cookies generated for that client are automatically returned in Header (we use POSTMAN), but when i try to make a request to the API (request that firstly goes to ID4 to get verified if the client cookie is not expired) apparently ID4 does not verify if the client cookie is or not expired, i can make as many requests as i like....
We really don't understand how to do it properly and we didn't find something that can help us...
Here are the snippets
Startup.cs
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookies";
options.DefaultChallengeScheme = "oidc";
options.RequireAuthenticatedSignIn = true;
})
.AddCookie("cookies", options =>
{
options.Cookie.Name = "cookie-bff";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(1);
options.Cookie.SecurePolicy = 0;
options.SlidingExpiration = true;
options.Events.OnSigningOut = async e =>
{
// revoke refresh token on sign-out
await e.HttpContext.RevokeUserRefreshTokenAsync();
};
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:4001/";
options.ClientId = "angular-client";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
/*
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api");
options.Scope.Add("offline_access");
*/
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
services.AddAccessTokenManagement(options =>
{
// client config is inferred from OpenID Connect settings
// if you want to specify scopes explicitly, do it here, otherwise the scope parameter will not be sent
options.Client.Scope = "write";
})
.ConfigureBackchannelHttpClient();
services.AddUserAccessTokenClient("user_client", client =>
{
client.BaseAddress = new Uri("https://localhost:5001/api/");
});
services.AddClientAccessTokenClient("client", configureClient: client =>
{
client.BaseAddress = new Uri("https://localhost:5001/api/");
});
Where we configure the Context for operation and configuration
services.AddIdentityServer(options => {
options.Authentication.CheckSessionCookieName = "cookie-bff";
options.Authentication.CookieLifetime = TimeSpan.FromMinutes(1);
options.Authentication.CookieSameSiteMode = SameSiteMode.Strict;
options.Authentication.CookieSlidingExpiration = true;
options.LowerCaseIssuerUri = false;
options.EmitScopesAsSpaceDelimitedStringInJwt = false;
})
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(configuration.GetConnectionString("LocalDevelopment"),
sql => sql.MigrationsAssembly(migrationAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(configuration.GetConnectionString("LocalDevelopment"),
sql => sql.MigrationsAssembly(migrationAssembly));
});
This is our LoginController(its work in progres, we are beginers)
[HttpPost]
[Route("/identityserver/login")]
public async Task<IActionResult> Post([Required, FromBody] LoginPostDTO Json)
{
var client = new HttpClient();
var response = await client.RequestTokenAsync(new TokenRequest
{
Address = "https://localhost:4001/connect/token",
GrantType = "client_credentials",
ClientId = "angular-client",
ClientSecret = "secret",
Parameters =
{
{ "scope", "read"},
{"openid","profile"}
}
});
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Name,"Company"),
new Claim(JwtClaimTypes.Role, "Administrator"),
new Claim(JwtClaimTypes.Subject, "Company")
};
var claimsIdentity = new ClaimsIdentity(claims, "cookies");
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
// Refreshing the authentication session should be allowed.
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
IsPersistent = false,
// Whether the authentication session is persisted across
// multiple requests. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.
IssuedUtc = DateTime.Now,
// The time at which the authentication ticket was issued.
RedirectUri = "https://localhost:4001/signin-oidc"
// The full path or absolute URI to be used as an http
// redirect response value.
};
await HttpContext.SignInAsync("cookies", new ClaimsPrincipal(claimsIdentity), authProperties);
var stringContent = new StringContent(JsonConvert.SerializeObject(Json), Encoding.UTF8, "application/json");
var api = new HttpClient();
api.SetBearerToken(response.AccessToken);
var apiResponse = await api.PostAsync("https://localhost:5001/api/login", stringContent);
return Ok(apiResponse.Content.ReadAsAsync(typeof(JObject)).Result);
}
}
And here is the Controller where we comunicate with the API after we login
public class Controller : ControllerBase
{
[HttpPost]
[Route("/identityserver/request")]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<IActionResult> Post([Required, FromBody]DTO Json)
{
var stringContent = new StringContent(JsonConvert.SerializeObject(Json), Encoding.UTF8, "application/json");
var api = new HttpClient();
api.SetBearerToken(await HttpContext.GetClientAccessTokenAsync());
var apiResponse = await api.PostAsync("https://localhost:5001/api/request/medical-request", stringContent);
return Ok(apiResponse.Content.ReadAsAsync(typeof(JObject)).Result);
}
}
Thank you in advance!
P.S This is my first post on stack :)
I am trying to use the HttpClient to access a REST service which requires NTLM authentication. However I keep getting a 401 Unauthorized.
My code looks like this
private static void Main()
{
var uri = new Uri("http://localhost:15001");
var credentialsCache = new CredentialCache { { uri, "NTLM", CredentialCache.DefaultNetworkCredentials } };
var handler = new HttpClientHandler { Credentials = credentialsCache };
var httpClient = new HttpClient(handler) { BaseAddress = uri, Timeout = new TimeSpan(0, 0, 10) };
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = httpClient.GetAsync("api/MyMethod").Result;
}
My target framework is netcoreapp2.0. If I change to net461, it will work. Not sure what I am doing wrong?
Microsoft has accepted this as a bug. Possibly a fix will be released with core 2.1
https://github.com/dotnet/corefx/issues/25988
Hi I'm trying to implement simple http proxy service using Owin infrastructure. This proxy must authenticate user with windows authentication, pull user's properties from the enterprise AD, add this info as a cookie value into original request and then redirect request to the application on the Internet (lets call it External Application).
I'm using HttpClient to send request to the External Application.
However HttpClient does not fit well for this scenario. It seems that the only allowed way to send cookie using it is to put them into CookieContainer and set this CookieContainer as a property of HttpClientHandler. It's ok when you have single user but in case of a proxy service cookie values from different users will mix and overwrite each other.
Is there any way to set cookie or CookieContainer per request? Or maybe there is a better way to redirect requests?
P.S. Here is some code:
Initialization of http handlers:
private void RegisterRoutes(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "Proxy",
routeTemplate: "{*path}",
handler: HttpClientFactory.CreatePipeline
(
innerHandler: new HttpClientHandler(),
handlers: new DelegatingHandler[]
{
new ProxyHandler()
}
),
defaults: new { path = RouteParameter.Optional },
constraints: null);
}
ProxyHandler
internal class ProxyHandler : DelegatingHandler
{
private readonly HttpClient _client;
public ProxyHandler()
{
var handler = new HttpClientHandler { ClientCertificateOptions = ClientCertificateOption.Automatic};
_client = new HttpClient(handler);
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var forwardUri = new UriBuilder(request.RequestUri);
forwardUri.Host = "localhost";
forwardUri.Port = 23016;
forwardUri.Scheme = Uri.UriSchemeHttp;
request.RequestUri = forwardUri.Uri;
request.Headers.Host = forwardUri.Host;
//Explicitly null it to avoid protocol violation
if (request.Method == HttpMethod.Get || request.Method == HttpMethod.Trace)
request.Content = null;
try
{
var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
//Explicitly null it to avoid protocol violation
if (request.Method == HttpMethod.Head)
response.Content = null;
return response;
}
catch (Exception ex)
{
var response = request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
string message = ex.Message;
if (ex.InnerException != null)
message += ':' + ex.InnerException.Message;
response.Content = new StringContent(message);
Trace.TraceError("Error:{0}", message);
return response;
}
}
private void SetCookies(HttpRequestMessage request)
{
var container = new CookieContainer();
var authCookieValue = "2EF91D8FD9EDC594F2DB82";
var authCookie = new Cookie("cookieByProxy", authCookieValue);
var targetUri = new Uri("http://localhost:23016/");
container.Add(targetUri, authCookie);
var cookieHeader = container.GetCookieHeader(targetUri);
if (!string.IsNullOrEmpty(cookieHeader))
request.Headers.TryAddWithoutValidation("Cookie", cookieHeader);//Overwriting cookie header with custom values. However cookie values are ignored by HttpClient (both old and new)
}
}
Found solution here: enter link description here
The trick is in explicitly setting UseCookie flag of the HttpClientHandler to false.
var handler = new HttpClientHandler { ClientCertificateOptions = ClientCertificateOption.Automatic, UseCookie = false };
I was playing around with .NET Core and building an API that utilizes payment APIs. There's a client certificate that needs to be added to the request for two-way SSL authentication.
How can I achieve this in .NET Core using HttpClient?
I have looked at various articles and found that HttpClientHandler doesn't provide any option to add client certificates.
I ran a fresh install for my platform (Linux Mint 17.3) following these steps: .NET Tutorial - Hello World in 5 minutes. I created a new console application targeting the netcoreapp1.0 framework, was able to submit a client certificate; however, I did receive "SSL connect error" (CURLE_SSL_CONNECT_ERROR 35) while testing, even though I used a valid certificate. My error could be specific to my libcurl.
I ran the exact same thing on Windows 7 and it worked exactly as needed.
// using System.Net.Http;
// using System.Security.Authentication;
// using System.Security.Cryptography.X509Certificates;
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
var result = client.GetAsync("https://apitest.startssl.com").GetAwaiter().GetResult();
I have a similar project where I communicate between services as well as between mobile and desktop with a service.
We use the Authenticode certificate from the EXE file to ensure that it's our binaries that are doing the requests.
On the requesting side (over simplified for the post).
Module m = Assembly.GetEntryAssembly().GetModules()[0];
using (var cert = m.GetSignerCertificate())
using (var cert2 = new X509Certificate2(cert))
{
var _clientHandler = new HttpClientHandler();
_clientHandler.ClientCertificates.Add(cert2);
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
var myModel = new Dictionary<string, string>
{
{ "property1","value" },
{ "property2","value" },
};
using (var content = new FormUrlEncodedContent(myModel))
using (var _client = new HttpClient(_clientHandler))
using (HttpResponseMessage response = _client.PostAsync($"{url}/{controler}/{action}", content).Result)
{
response.EnsureSuccessStatusCode();
string jsonString = response.Content.ReadAsStringAsync().Result;
var myClass = JsonConvert.DeserializeObject<MyClass>(jsonString);
}
}
I then use the following code on the action that gets the request:
X509Certificate2 clientCertInRequest = Request.HttpContext.Connection.ClientCertificate;
if (!clientCertInRequest.Verify() || !AllowedCerialNumbers(clientCertInRequest.SerialNumber))
{
Response.StatusCode = 404;
return null;
}
We rather provide a 404 than a 500 as we like those that are trying URLs to get a bad request rather then let them know that they are "on the right track"
In .NET Core, the way to get the certificate is no longer by going over Module. The modern way that might work for you is:
private static X509Certificate2? Signer()
{
using var cert = X509Certificate2.CreateFromSignedFile(Assembly.GetExecutingAssembly().Location);
if (cert is null)
return null;
return new X509Certificate2(cert);
}
After a lot of testing with this issue I ended up with this.
Using SSL, I created a pfx file from the certificate and key.
Create a HttpClient as follows:
_httpClient = new(new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12,
ClientCertificates = { new X509Certificate2(#"C:\kambiDev.pfx") }
});
I'm not using .NET for my client, but server side it can be configured simply via IIS by deploying my ASP.NET Core website behind IIS, configuring IIS for HTTPS + client certificates:
IIS client certificate setting:
Then you can get it simply in the code:
var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();
if(clientCertificate!=null)
return new ContentResult() { Content = clientCertificate.Subject };
It's working fine for me, but I'm using curl or chrome as clients, not the .NET ones. During the HTTPS handshake, the client gets a request from the server to provide a certificate and send it to the server.
If you are using a .NET Core client, it can't have platform-specific code and it would make sense if it couldn't connect itself to any OS specific certificates store, to extract it and send it to the server. If you were compiling against .NET 4.5.x then it seems easy:
Using HttpClient with SSL/TLS-based client side authentication
It's like when you compile curl. If you want to be able to connect it to the Windows certificates store, you have to compile it against some specific Windows library.
Can be used for both .NET Core 2.0< and .NET Framework 4.7.1<:
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1
Make all configuration in Main() like this:
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
string env="", sbj="", crtf = "";
try
{
var whb = WebHost.CreateDefaultBuilder(args).UseContentRoot(Directory.GetCurrentDirectory());
var environment = env = whb.GetSetting("environment");
var subjectName = sbj = CertificateHelper.GetCertificateSubjectNameBasedOnEnvironment(environment);
var certificate = CertificateHelper.GetServiceCertificate(subjectName);
crtf = certificate != null ? certificate.Subject : "It will after the certification";
if (certificate == null) // present apies even without server certificate but dont give permission on authorization
{
var host = whb
.ConfigureKestrel(_ => { })
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseConfiguration(configuration)
.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
})
.Build();
host.Run();
}
else
{
var host = whb
.ConfigureKestrel(options =>
{
options.Listen(new IPEndPoint(IPAddress.Loopback, 443), listenOptions =>
{
var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.AllowCertificate,
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
ServerCertificate = certificate
};
listenOptions.UseHttps(httpsConnectionAdapterOptions);
});
})
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls("https://*:443")
.UseStartup<Startup>()
.UseConfiguration(configuration)
.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
})
.Build();
host.Run();
}
Log.Logger.Information("Information: Environment = " + env +
" Subject = " + sbj +
" Certificate Subject = " + crtf);
}
catch(Exception ex)
{
Log.Logger.Error("Main handled an exception: Environment = " + env +
" Subject = " + sbj +
" Certificate Subject = " + crtf +
" Exception Detail = " + ex.Message);
}
}
Configure file startup.cs like this:
#region 2way SSL settings
services.AddMvc();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
})
.AddCertificateAuthentication(certOptions =>
{
var certificateAndRoles = new List<CertficateAuthenticationOptions.CertificateAndRoles>();
Configuration.GetSection("AuthorizedCertficatesAndRoles:CertificateAndRoles").Bind(certificateAndRoles);
certOptions.CertificatesAndRoles = certificateAndRoles.ToArray();
});
services.AddAuthorization(options =>
{
options.AddPolicy("CanAccessAdminMethods", policy => policy.RequireRole("Admin"));
options.AddPolicy("CanAccessUserMethods", policy => policy.RequireRole("User"));
});
#endregion
The certificate helper
public class CertificateHelper
{
protected internal static X509Certificate2 GetServiceCertificate(string subjectName)
{
using (var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
{
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(
X509FindType.FindBySubjectDistinguishedName, subjectName, true);
X509Certificate2 certificate = null;
if (certCollection.Count > 0)
{
certificate = certCollection[0];
}
return certificate;
}
}
protected internal static string GetCertificateSubjectNameBasedOnEnvironment(string environment)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.{environment}.json", optional: false);
var configuration = builder.Build();
return configuration["ServerCertificateSubject"];
}
}
If you look at the .NET Standard reference for the HttpClientHandler class, you can see that the ClientCertificates property exists, but is hidden due to the use of EditorBrowsableState.Never. This prevents IntelliSense from showing it, but will still work in code that uses it.
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get; }
I thought the best answer for this was provided here.
By utilizing the X-ARR-ClientCert header you can provide the certificate information.
An adapted solution is here:
X509Certificate2 certificate;
var handler = new HttpClientHandler {
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12
};
handler.ClientCertificates.Add(certificate);
handler.CheckCertificateRevocationList = false;
// this is required to get around self-signed certs
handler.ServerCertificateCustomValidationCallback =
(httpRequestMessage, cert, cetChain, policyErrors) => {
return true;
};
var client = new HttpClient(handler);
requestMessage.Headers.Add("X-ARR-ClientCert", certificate.GetRawCertDataString());
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.SendAsync(requestMessage);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var keyResponse = JsonConvert.DeserializeObject<KeyResponse>(responseContent);
return keyResponse;
}
And in your .net core server's Startup routine:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddCertificateForwarding(options => {
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) => {
X509Certificate2 clientCertificate = null;
try
{
if (!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = ConvertHexToBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
}
catch (Exception)
{
// invalid certificate
}
return clientCertificate;
};
});
}