Getting client certificate - c#

I need to implement Client Certificate authentication on some of the endpoints in my .NET 5 Web API. So I don't want to enable HTTPS across all endpoint as described here in the MS docs. I am using Kestrel on my local machine and not IIS express or IIS.
I have tried the following three methods with no luck on either of them:
var clientCertHeaders = context.HttpContext.Request.Headers;
This one returns the normal headers for the request but no certificate.
var clientCert = context.HttpContext.Connection.ClientCertificate;
var clientCertAsync = context.HttpContext.Connection.GetClientCertificateAsync().Result;
These two both return null.
I've tried applying the following to my services:
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if(!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = Encoding.UTF8.GetBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
Even with that enabled in my services I am not retrieving the client certificate.
I am using Postman to make the requests to the API requests.

You need to configure Kestrel to allow client certificates in the program.cs The default value is ClientCertificateMode.NoCertificate so in your ConfigureWebHostDefaults you need to change that to ClientCertificateMode.AllowCertificate.
Here's an edited chunk of code from the docs you sent where I did that:
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
o.ClientCertificateMode =
ClientCertificateMode.AllowCertificate);
});
});
}

Related

Kestrel ASP.Net Core 6.0 handling of expired client certificates - different behaviour when CustomTrustStore is used

I am using a self-hosted Kestrel Asp Net Core web server and am testing the behaviour when a client sends an expired certificate. The test is peformed with two scenarios, one with the root certificate imported into the Windows certificate store, and one using an application internal custom certificate store. The server uses a pfx file to construct a X509Certificate2 object.
Here is the code sample:
private void InitServices(WebApplicationBuilder builder, bool useCustomCertificateStore)
{
var webBuilder = builder.WebHost;
// load certificate from pfx file
var rootCert = new X509Certificate2(rootCertPfxFilePath, rootCertPfxFilePassword);
webBuilder.ConfigureKestrel(o =>
{
if (useCustomCertificateStore)
{
o.ConfigureHttpsDefaults(o =>
{
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
o.AllowAnyClientCertificate();
o.CheckCertificateRevocation = false;
});
}
else
{
o.ConfigureHttpsDefaults(o =>
{
o.ServerCertificate = rootCert;
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
}
});
webBuilder.UseKestrel(o =>
{
o.Listen(IPAddress.Parse(hostSettings.HttpsEndPoint.IpAddr), hostSettings.HttpsEndPoint.Port,
listenOptions =>
{
HttpsConnectionAdapterOptions httpsConnectionAdapterOptions = null;
if (useCustomCertificateStore)
{
httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ServerCertificate = rootCert
};
httpsConnectionAdapterOptions.AllowAnyClientCertificate();
}
else
{
httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
SslProtocols = System.Security.Authentication.SslProtocols.None,
ServerCertificate = rootCert,
ClientCertificateValidation = (x509Cert, x509Chain, sslErrors) =>
{
logger.LogInformation($"Certificate validation request");
if (sslErrors == SslPolicyErrors.None)
{
return true;
}
else
{
logger.LogError($"Certificate validation failed because of SslPolicyErrors <{sslErrors.ToString()}>");
}
return false;
}
};
}
listenOptions.UseHttps(httpsConnectionAdapterOptions);
});
});
var services = builder.Services;
services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
// custom certificate store, if windows certificate store should not be used!
if (useCustomCertificateStore)
{
options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
options.CustomTrustStore = new X509Certificate2Collection { rootCert };
options.RevocationMode = X509RevocationMode.NoCheck;
}
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
logger?.LogError($"OnAuthenticationFailed with <{context.Exception.Message}> (host <{context.Request.Host}>, method <{context.Request.Method}>, path <{context.Request.Path}>)");
context.Fail("invalid cert");
return Task.CompletedTask;
}
};
});
// add authorization to require auth by default for all routes
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
.....
}
I am using curl to call different endpoints on the web server. The tests with valid certificates are executed successfully. The tests with an expired certificate show a different behaviour of the kestrel web server, dependently if the custom certificate store is used or not (in this case the windows certificate store contains the certificate).
If the PC where the kestrel web server is running has the root certificate imported into the Windows Certificate Store, the kestrel web server sends an error message pertaining to the TLS handshake -> TLS alert certificate expired, and no http response is sent at all.
If the kestrel web server uses a custom certificate store, the kestrel web server sends a 403 http response.
Why is the behaviour different ? Can the kestrel web server be configured to always send a 403 http response in such scenarios?

Error of healthCheck when the service is in IIS

I'm using the healthCheck to verify the state of the services but I have an error when this service is in IIS.
In ConfigureServices:
services.AddHealthChecksUI(s =>
{
s.AddHealthCheckEndpoint("Validations", "/CheckPoints");
}).AddInMemoryStorage();
services.AddHealthChecks();
In Configure
builder.MapHealthChecksUI(setupOptions: setup =>
{
setup.UIPath = "/CheckPointsUI";
});
builder.MapHealthChecks("/CheckPoints", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
builder.MapHealthChecks("/Status");
When I run the service in localhost all works good but the same code in IIS pool I have the next error:
An invalid request URI was provided. The request URI must either be an absolute URI or BaseAdress must be set
I don't know why it behaves differently.

How to use Client SSL Certificates with .Net Core

I recently wanted to configure a .net core website to use client ssl certificate authentication
I couldn't find a good example so I did a bit of research and decided to post the results here for others.
In .net core 2.2 you can configure client certificates as an option inside the .UseHttps method while configuring Kestrel in Program.cs
With this configuration when a user pulls up the site in the browser the browser will present a dialog asking the user to select a client certificate for authentication. If the certificate is invalid, the server will return a HTTP 495 SSL Certificate Error
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Listen(IPAddress.Loopback, 5022);
options.Listen(IPAddress.Loopback, 5023, listenOptions =>
{
listenOptions.UseHttps((httpsOptions) =>
{
var certFileName = "server_cert.pfx";
var contentRoot = context.HostingEnvironment.ContentRootPath;
X509Certificate2 serverCert;
var path = Path.Combine(contentRoot, certFileName);
serverCert = new X509Certificate2(path, "<server cert password>");
httpsOptions.ServerCertificate = serverCert;
// this is what will make the browser display the client certificate dialog
httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
httpsOptions.CheckCertificateRevocation = false;
httpsOptions.ClientCertificateValidation = (certificate2, validationChain, policyErrors) =>
{
// this is for testing non production certificates, do not use these settings in production
validationChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
validationChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
validationChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
validationChain.ChainPolicy.VerificationTime = DateTime.Now;
validationChain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
validationChain.ChainPolicy.ExtraStore.Add(serverCert);
var valid = validationChain.Build(certificate2);
if (!valid)
return false;
// only trust certs that are signed by our CA cert
valid = validationChain.ChainElements
.Cast<X509ChainElement>()
.Any(x => x.Certificate.Thumbprint == serverCert.Thumbprint);
return valid;
};
});
});
});
}

Authorization error (401) in .net core when calling api to api in windows authentication

Have 2 Web API's created using .Net Core 2.0 and hosted internally in IIS under windows authentication (anonymous disabled) on same server. Both API's run on same service account as well with appropriate permissions/rolegroups in Active Directory. However, get 401 unauthorized error when consuming one API from the other. Using HTTPClient to make API calls. Note that, it works when accessing the 2nd API endpoint directly via browser but not from another API.
Decorated with Authorize filter in controller
[Authorize(Policy = "ValidRoleGroup")]
Start up code in ConfigureServices in both api services as below.
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.AddPolicy("ValidRoleGroup", policy => policy.RequireRole(Configuration["SecuritySettings:ValidRoleGroup"]));
});
services.AddMvc(configure =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
configure.Filters.Add(new AuthorizeFilter(policy));
});
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
options.ForwardClientCertificate = true;
});
services.AddMvc();
services.AddScoped<HttpClient>(c => new HttpClient(new HttpClientHandler()
{
UseDefaultCredentials = true,
PreAuthenticate = true,
ClientCertificateOptions = ClientCertificateOption.Automatic,
}));
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
The 401 errors went away after adding registry entries as described in below article (Method 1)
https://support.microsoft.com/en-us/help/896861/you-receive-error-401-1-when-you-browse-a-web-site-that-uses-integrate
Note that the Value data should be your actual domain URL (XXX.com) and not machine name.

IdentityServer3 - rejected because invalid CORS path

We have an ASP.NET MVC application that is authenticating without issue against IdentityServer3, however the web API part of the application using ApiController's start to fail if the user waits before proceeding with AJAX functionality after about 3 minutes (before 3 mins everything seems fine).
The errors seen in Chrome are:
XMLHttpRequest cannot load
https://test-auth.myauthapp.com/auth/connect/authorize?client_id=ecan-farmda…gwLTk5ZjMtN2QxZjUyMjgxNGE4MDg2NjFhZTAtOTEzNi00MDE3LTkzNGQtNTc5ODAzZTE1Mzgw.
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://test.myapp.com' is therefore not allowed
access.
On IE I get the following errors:
SCRIPT7002: XMLHttpRequest: Network Error 0x4c7, The operation was
canceled by the user.
Looking at IdentityServer3's logs I'm seeing entries like so:
2015-08-10 16:42 [Warning]
(Thinktecture.IdentityServer.Core.Configuration.Hosting.CorsPolicyProvider)
CORS request made for path: /connect/authorize from origin:
http://test.myapp.com but rejected because invalid CORS path
In the IdentityServer3 web application I'm giving clients AllowedCorsOrigins:
Thinktecture.IdentityServer.Core.Models.Client client = new Thinktecture.IdentityServer.Core.Models.Client()
{
Enabled = configClient.Enabled,
ClientId = configClient.Id,
ClientName = configClient.Name,
RedirectUris = new List<string>(),
PostLogoutRedirectUris = new List<string>(),
AllowedCorsOrigins = new List<string>(),
RequireConsent = false, // Don't show consents screen to user
RefreshTokenExpiration = Thinktecture.IdentityServer.Core.Models.TokenExpiration.Sliding
};
foreach (Configuration.RegisteredUri uri in configClient.RedirectUris)
{
client.RedirectUris.Add(uri.Uri);
}
foreach (Configuration.RegisteredUri uri in configClient.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(uri.Uri);
}
// Quick hack to try and get CORS working
client.AllowedCorsOrigins.Add("http://test.myapp.com");
client.AllowedCorsOrigins.Add("http://test.myapp.com/"); // Don't think trailing / needed, but added just in case
clients.Add(client);
And when registering the service I add a InMemoryCorsPolicyService:
app.Map("/auth", idsrvApp =>
{
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<AuthContext>(resolver => AuthObjects.AuthContext));
factory.Register(new Registration<AuthUserStore>());
factory.Register(new Registration<AuthRoleStore>());
factory.Register(new Registration<AuthUserManager>());
factory.Register(new Registration<AuthRoleManager>());
// Custom user service used to inject custom registration workflow
factory.UserService = new Registration<IUserService>(resolver => AuthObjects.AuthUserService);
var scopeStore = new InMemoryScopeStore(Scopes.Get());
factory.ScopeStore = new Registration<IScopeStore>(scopeStore);
var clientStore = new InMemoryClientStore(Clients.Get());
factory.ClientStore = new Registration<IClientStore>(clientStore);
var cors = new InMemoryCorsPolicyService(Clients.Get());
factory.CorsPolicyService = new Registration<ICorsPolicyService>(cors);
...
var options = new IdentityServerOptions
{
SiteName = "Authentication",
SigningCertificate = LoadCertificate(),
Factory = factory,
AuthenticationOptions = authOptions
};
...
});
I do note that the IdentityServer3 log entries say "CORS request made for path: /connect/authorize" rather than "CORS request made for path: /auth/connect/authorize". But looking through the IdentityServer3 source code suggests this probably isn't the issue.
Perhaps the InMemoryCorsPolicyService isn't being picked up?
Any ideas of why things aren't working for the AJAX called ApiController?
Thinktecture.IdevtityServer3 v1.6.2 has been installed using NuGet.
Update
I'm having a conversation with the IdentityServer3 developer, but am still having an issue reaching a resolution. In case it helps:
https://github.com/IdentityServer/IdentityServer3/issues/1697
Did you try adding https url also?- client.AllowedCorsOrigins.Add("https://test.myapp.com");
The documentation of IdentityServer says you should configure it on the client:
AllowedCorsOrigins = ... // Defaults to the discovery, user info, token, and revocation endpoints.
https://docs.duendesoftware.com/identityserver/v6/reference/options/#cors
CORS is a nightmare!
It's a browser thing which is why you're witnessing different behaviour in IE than in Chrome.
There are (at least) two ways that CORS is configured on the server. When a client makes a request with the Origin header you have to tell the server whether or not to accept it -- if accepted then the server adds the Access-Control-Allow-Origin header to the response for the browser.
In MVC / webAPI you have to add CORS services, set a CORS policy, and then .UseCors something like this:
builder.Services.AddCors((options =>
{
if (settings.AllowedCorsOrigins.Length > 0)
{
options.AddDefaultPolicy(builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().WithOrigins(settings.AllowedCorsOrigins);
});
}
if (isDevelopment)
{
options.AddPolicy("localhost", builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed((string origin) => { return origin.Contains("localhost"); }); });
}
});
and
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseCors("localhost");
}
Typically, you want the list of allowed hosts as an array of strings in your appsettings.json. And watch out for the boobytrap with SetIsOriginAllowedToAllowWildcardSubdomains.
As well as this, IdentityServer has its own additional CORS settings which are applied in addition to the standard MVC/webAPI settings. These are in the ClientCorsOrigin table and this doesn't support wildcard subdomains. You can sidestep this whole boobytrap by implementing your own ICorsPolicyService to use the same settings from your appsettings.json something like this
public class CorsPolicyService : ICorsPolicyService
{
private readonly CorsOptions _options;
public CorsPolicyService(IOptions<CorsOptions> options)
{
_options = options.Value;
}
private bool CheckHost(string host)
{
foreach (string p in _options.AllowedCorsOrigins)
{
if (Regex.IsMatch(host, Regex.Escape(p).Replace("\\*", "[a-zA-Z0-9]+"))) // Hyphen?
{
return true;
}
}
return false;
}
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(CheckHost(origin));
}
}

Categories