Client certificate not included by HttpClientHandler in .net core - .net-core

I'm having difficulties sending a certificate using HttpClientHandler because the certificate simply won't appear on Server's request. The certificate has the proper EKU for server and client authentication, and Key Usage of "Digital Signature". #davidsh regarded the issue 26531 for the lack of logging that HttpClient had but running my project in Visual Studio (with logs set to Trace and using dotnet 3.1.401) no output error came out. I'm not very familiar at all with logman but I ran it when the issue supposed to happen as I executed my code and nothing from the log stood out indicating what the problem could be. Running out of options to test the code I attempted to add a certificate without the private key on the client request to see if the httpClientHandler.ClientCertificates.Add ... would throw any error saying something like "You need a certificate with private key to sign your request", shouldn't it say anything?
On client:
services.AddHttpClient<ILetterManClient, LetterManClient.LetterManClient>()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri(configuration.GetValue<string>("Microservices:LetterManAPI"));
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
HttpClientHandler httpClientHandler = new HttpClientHandler();
httpClientHandler.ServerCertificateCustomValidationCallback = ValidateServiceCertficate;
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
clientCertificate = new X509Certificate2("client_cert.pfx", "developer");
httpClientHandler.ClientCertificates.Add(clientCertificate);
return httpClientHandler;
});
On server:
public class ValidateClientCertificates : TypeFilterAttribute
{
public ValidateClientCertificates() : base(typeof(ValidateClientCertificatesImpl))
{
}
private class ValidateClientCertificatesImpl : IAsyncAuthorizationFilter
{
X509Certificate2 clientCertificate;
public ValidateClientCertificatesImpl(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
clientCertificate = new X509Certificate2("client_cert.crt");
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var certificate = await context.HttpContext.Connection.GetClientCertificateAsync();
if ((certificate == null) || (!certificate.Thumbprint.Equals(clientCertificate.Thumbprint)))
{
context.Result = new UnauthorizedObjectResult("");
return;
}
}
}
}
Side note:
I've been also trying to debug my project using code compiled from corefx repo to see what's going but Visual Studio insists reference the code from local installed sdk instead of the project from corefx that it's referencing it but this is another issue.
I've created this project that simulates the issue. It creates the certificates and it has two projects with one service and another client implemented.
Any help will be very welcomed.

These are the guidelines for Kestrel to require Client certificate but it assumes that the CA is installed in the machine otherwise you have to specify the client certificate directly when configuring Kestrel server as follows:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o => {
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
o.ClientCertificateValidation = ValidateClientCertficate;
});
});
});
public static Func<X509Certificate, X509Chain, SslPolicyErrors, bool> ValidateClientCertficate =
delegate (X509Certificate serviceCertificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
X509Certificate2 clientCertificate;
clientCertificate = new X509Certificate2("client.crt");
if (serviceCertificate.GetCertHashString().Equals(clientCertificate.Thumbprint))
{
return true;
}
return false;
};
Unfortunately, you can't require Client certificates for a specific route as I intended.

Related

SignalR gives 404 when using WebSockets or ServerSentEvents for some users but not for others

SignalR gives me 404 when trying to connect for some users. URLs are the same except for access_token.
It is stable reproducible per user (I mean that some users are stable OK, some users are stable 404).
access_token parsed jwt diff (left is OK user, right gets 404):
I did a trace level of logs and have next:
For the OK user:
For the user that gets 404:
Note: URLs under black squares are the same.
Front End is Angular 9 with package "#microsoft/signalr": "^3.1.8", and here's the code that builds the connection:
private buildHub(): HubConnection {
console.log(this.authService.accessToken);
let builder = new HubConnectionBuilder()
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.withUrl('ws/notificationHub', {
accessTokenFactory: () => this.authService.accessToken
});
if (this.debugMode) {
builder = builder.configureLogging(LogLevel.Trace);
}
return builder.build();
}
Backend is using next code in Startup for configuring signalR hub:
In public void ConfigureServices(IServiceCollection services):
services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerSettings.ContractResolver = new DefaultContractResolver();
});
In public void Configure(IApplicationBuilder app, IHostingEnvironment env):
app.UseSignalR(route =>
{
route.MapHub<NotificationHub>("/ws/notificationHub");
});
Also we use custom authentication, so we have Authorize attribute for the Hub class:
[Authorize]
public class NotificationHub: Hub<INotificationHubClient>
and this code in public void ConfigureServices(IServiceCollection services):
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = identityServerSettings.Url;
options.Audience = identityServerSettings.ApiScopeName;
options.RequireHttpsMetadata = identityServerSettings.RequireHttpsMetadata;
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/ws"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Unfortunately, I don't have the full access to the environment where it is reproducible, but I can request to see any settings or try to make some changes.
What else can I try to troubleshoot the issue?
UPDATE: negotiate is fine for both users.
I had this issue recently, after the size of my JWT increased. I found that in my case the 404 error was being thrown by IIS because the query string exceeded the limit of 2048. After increasing the query string max length, my issue was resolved.

Can you run a asp.net core 3.0 gRPC CLIENT in IIS? (possibly on Azure?)

I've read a lot of conflicting information about this and it seems people are not 100% clear on what is possible and what is not. I am certain that you cannot host a gRPC server app in IIS due to the HTTP/2 limitations. The documentation is pretty clear. However, I want to use IIS as a reverse proxy, with the internal side communicating using gRPC. So the client would be in IIS, not the server. I assumed that since the communication at this point (i.e. the back end) was not funneled through IIS, there would be no issue with this. However, I keep seeing mixed answers.
I have created a dumb webapp that is hosted in IIS Express and can successfully post to my service running on Kestrel with gRPC.
Client code sample below. The SubmitButton is just a form post on the razor page.
public async void OnPostSubmitButton()
{
// The port number(5001) must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
Server code is the boilerplate template for gRPC but looks like this:
namespace grpcGreeter
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
namespace grpcGreeter
{
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
}
This works. But, because I keep seeing mixed information saying it that it won't, I am not certain that once I go to deploy the client code (i.e. the reverse proxy), if I will run into problems. I would like to use a host like Azure...but don't know if it's possible or not.
Any clarity on the subject would be greatly appreciated.
As far as I know, we could use asp.net core mvc or razor page application as the client to call the grpc server.
But gRPC client requires the service to have a trusted certificate when you hosted the application on remote server IIS.
If you don't have the permission to install the certificate, you should uses HttpClientHandler.ServerCertificateCustomValidationCallback to allow calls without a trusted certificate.
Notice: this will make the call not security.
Additional configuration is required to call insecure gRPC services with the .NET Core client. The gRPC client must set the System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport switch to true and use http in the server address.
Code as below:
AppContext.SetSwitch(
"System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
var httpClientHandler = new HttpClientHandler();
// Return `true` to allow certificates that are untrusted/invalid
httpClientHandler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var httpClient = new HttpClient(httpClientHandler);
var channel = GrpcChannel.ForAddress("https://localhost:5001",
new GrpcChannelOptions { HttpClient = httpClient });
var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });

Accessing HttpClientHandler in the same place as HttpClient is created

I have a service requesting an URL and validating the server SSL certificate. The code has been running smoothly with HttpWebRequest in full .NET framework, but now I want to migrate it to HttpClient and .NET Core. I can get the certificate like this (the approach is recommended in multiple blog posts and stack overflow answers):
X509Certificate2 cert = null;
var httpClient = new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
cert = certificate;
return true;
}
});
httpClient.GetAsync(...);
The issue here is that I constantly create new HttpClient instances, which isn't recommended. I want to move to HttpClientFactory, why I add the following in my setup code:
services
.AddHttpClient("sslclient", x =>
{
...
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
return true;
}
});
The challenge now is that the code creates the client no longer has access to ServerCertificateCustomValidationCallback:
var httpClient = httpClientFactory.CreateClient("sslclient");
Anyone know how to solve this?
Someone at Reddit suggested the following solution. Once the call to AddHttpClient has been made, it is no longer possible to modify the HttpClientHandler. It is possible to share a resource, though:
var certificates= new ConcurrentDictionary<string, X509Certificate2>();
services.AddSingleton(certificates);
services
.AddHttpClient("sslclient", x =>
{
...
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
certificates.TryAdd(request.RequestUri.Host, new X509Certificate2(certificate));
return true;
}
});
In the code making the HTTP request, you'd need to inject the certificates dictionary as well. Once the request has been made, you can check for a certificate in the dictionary:
var response = await httpClient.GetAsync(url);
if (certificates.ContainsKey(uri.Host))
{
// Happy days!
}

Blazor Request blocked by CORS policy

I am trying to send a request from a Blazor(client-side) client to a server and i keep getting this error:
Access to fetch at '[route]' (redirected from '[other route]') from
origin '[origin route]' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource. If an opaque response serves your needs, set the request's
mode to 'no-cors' to fetch the resource with CORS disabled.
On the server i have already added the CORS extension in the pipeline to no avail:
Server Startup
public void ConfigureServices(IServiceCollection services) {
services.AddCors();
services.AddResponseCompression(options => {
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
{
MediaTypeNames.Application.Octet,
WasmMediaTypeNames.Application.Wasm,
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials());
app.UseResponseCompression();
app.UseMvc();
app.UseBlazor<Client.Startup>();
}
Blazor Client request
public async Task<Catalog> GetCatalogAsync() {
try {
HttpRequestMessage message = new HttpRequestMessage {
RequestUri = new Uri(BASE_PATH + Routes.GET_CATALOG), //BASE_PATH= 172.XX.XX.XX:8600
Method = HttpMethod.Get
};
var resp = await this.client.SendAsync(message); // client is HttpClient
var resultString = await resp.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Catalog>(resultString);
return data;
} catch (Exception ex) {
throw;
}
}
Controller
[HttpGet]
[Route(Routes.GET_CATALOG)]
public async Task<Catalog> GetCatalogAsync() {
try {
var registry = await this.adminService.GetCatalogAsync();
return registry;
} catch (Exception ex) {
throw;
}
}
POCO
[Serializeable]
public struct Catalog{
}
What else can i do to be able to reach my server? Is it due to Blazor ?
As you can see i have already added the UseCors(...).
P.S
I have published my Blazor Server project together with the Client.They are in the same directory.This folder i placed it on a computer,and i am trying from my computer to open blazor : 172.168.18.22:8600/
Update
I have also tried adding headers to my HttpRequestMessage to no avail:
HttpRequestMessage message = new HttpRequestMessage {
RequestUri = new Uri(BASE_PATH + Routes.GET_CATALOG),
Method = HttpMethod.Get,
};
message.Headers.Add("Access-Control-Allow-Origin","*");
message.Headers.Add("Access-Control-Allow-Credentials", "true");
message.Headers.Add("Access-Control-Allow-Headers", "Access-Control-Allow-Origin,Content-Type");
#Bercovici Adrian, why do you add CORS support to your App ? Do you make cross origin requests ? If you don't, don't try to solve the issue by adding unnecessary configuration that may lead to more subtle bugs.
As usual, without seeing a repo of this app, can't help you any further.
Update:
What is this UseBlazor ?
You should upgrade your app to the latest version...
New Update:
Sorry, but I'm using the current preview version of Blazor
Startup class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddNewtonsoftJson();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
**// Instead of UseBlazor**
app.UseClientSideBlazorFiles<Client.Startup>();
app.UseStaticFiles();
app.UseRouting();
**// This configure your end points**
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
});
}
}
Note that I've removed the configuration of CORS as your client and server share the same domain. Please use the docs how to configure CORS appropriately.
Try this and see if it is working for you (I guess your issue is related to the configuration of the endpoints. Somehow, it seems to me that because you did not configure the endpoints, your request is redirected, and thus you get the message displayed by you above.)
Next to do is to check if your http request was appropriately cooked. But first checks the end points.
Somehow the problem was due to a very old client version that was cached on the browser.Never again will i forget to clear the browser cache after this problem.
Thank you all for your help and support !
Check that you do not send HTTP requests when running from HTTPS. For example if you send requests to http://172.168.18.22:8600 when your application was opened in https://172.168.18.22:8600 you may have an issue.
you need to specify your policy name in the middleware.
builder.Services.AddCors(policy =>{
policy.AddPolicy("Policy_Name", builder =>
builder.WithOrigins("https://*:5001/")
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyOrigin()
);});
// Configure the HTTP request pipeline.
app.UseCors("Policy_Name");

OpenIddict - hosting auth server and web api resource in same project

I want to implement an OpenIdConnect/Oauth2 server using OpenIddict in order to secure a .NET core API app. Most examples I have seen implement these as separate projects.
The client app is a SPA and we are using implicit flow.
I have based my solution on the code shown in the OpenIddict samples here:
https://github.com/openiddict/openiddict-samples
For the project I am working on it would ideally have the Auth server and API to use the same port and be in the same project. ( One of the customer's requirements is that they don't want another server to configure since they own the API resource and it will be on the same server)
I have configured OpenIddict and combined it with our API project. Almost everything works correctly - the API endpoints are protected with the [Authorize] attribute and prevent access to protected API end points. However, when the API resource is protected, instead of returning a 401 Unauthorized HTTP status code, the returned result is the HTML Login page of the Auth server itself.
Here is the relevant setup code in my Startup.cs file:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseIdentity();
app.UseCors("AllowAll");
//app.UseCors(builder =>
//{
// builder.AllowAnyOrigin();//)WithOrigins("http://localhost:9000");
// builder.WithMethods("GET","POST", "PUT", "DELETE", "OPTIONS");
// builder.WithHeaders("Authorization");
//});
app.UseWhen(context => !context.Request.Path.StartsWithSegments("/api"), branch =>
{
branch.UseIdentity();
});
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch =>
{
branch.UseOAuthValidation();
});
app.UseOpenIddict();
#region Adding resource config here (api)
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseOAuthIntrospection(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.Authority = "http://localhost:5000";
options.Audiences.Add("resource-server-1");
options.ClientId = "resource-server-1";
options.ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342";
});
//app.UseCors(builder => {
// builder.WithOrigins("http://localhost:9000");
// builder.WithMethods("GET");
// builder.WithHeaders("Authorization");
//});
#endregion
app.UseMvcWithDefaultRoute();
// Seed the database with the sample applications.
// Note: in a real world application, this step should be part of a setup script.
InitializeAsync(app.ApplicationServices, CancellationToken.None).GetAwaiter().GetResult();
}
private async Task InitializeAsync(IServiceProvider services, CancellationToken cancellationToken)
{
// Create a new service scope to ensure the database context is correctly disposed when this methods returns.
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
//await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<OpenIddictApplicationManager<OpenIddictApplication>>();
if (await manager.FindByClientIdAsync("MySPA", cancellationToken) == null)
{
var application = new OpenIddictApplication
{
ClientId = "MySPA",
DisplayName = "MySPA",
LogoutRedirectUri = "http://localhost:9000/signout-oidc",
RedirectUri = "http://localhost:9000/signin-oidc"
};
await manager.CreateAsync(application, cancellationToken);
}
if (await manager.FindByClientIdAsync("resource-server-1", cancellationToken) == null)
{
var application = new OpenIddictApplication
{
ClientId = "resource-server-1"
};
await manager.CreateAsync(application, "846B62D0-DEF9-4215-A99D-86E6B8DAB342", cancellationToken);
}
}
}
Not sure how to implement these both side by side in the same project. As mentioned it all "works" except the API is returning the HTML login page and not a desired HTTP status
app.UseIdentity(); is present twice in your pipeline, which defeats the whole purpose of using branch.UseIdentity() in a app.UseWhen() branching builder (i.e making sure the cookies middleware registered by Identity are not invoked for your API endpoints).
Remove the first occurrence and it should work.
You set the AutomaticChallenge to true. According to the documentation
this flag indicates that the middleware should redirect the browser to the LoginPath or AccessDeniedPath when authorization fails.
So by setting this to false it will not redirect to the login.

Resources