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;
};
});
});
});
}
Related
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?
I'm trying to add an ssl certificate to .net core project with Kestral.
I have tried to configure ssl certificate pfx file path and the password on appsettings.json file, this solution worked for me but I want to store the password on a vault so I have tried to set the cert path and password on the CreateHostBuilder method using
webBuilder.ConfigureKestrel(options => {
var port = 5002;
var pfxFilePath = #"path\certificate.pfx";
var pfxPassword = "pass";
options.Listen(IPAddress.Any, port, listenOptions => {
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
listenOptions.UseHttps(pfxFilePath, pfxPassword);
});
});
but I got ERR_CONNECTION_REFUSED error message.
We have a WebAPI webapp which should support authentication via client certificate and authentication via Microsoft Identity/OAuth2. With our current implementation, it seems that the Microsoft Identity Authentication overrules the Client Certificate Authentication. If we just add the Microsoft Identity Authentication in first place, then the overruling behaves the opposite round.
The Program.cs for authentication looks currently like this:
var builder = WebApplication.CreateBuilder(args);
// 1. setup Certificate Authentication
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
{
// https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
// since with that authentication we have no retrieved authorization from anywhere, we set it explicitly - for now ...
new Claim("scp", "GeneralScope")
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
},
OnChallenge = x =>
{
return Task.CompletedTask;
}
};
});
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(opts =>
{
opts.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
opts.ClientCertificateValidation = (cert, chain, policyErrors) =>
{
// TODO validate certificate
return true;
};
});
});
// 2. setup Microsoft Identity Authentication
var jwtAuthentication = builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
jwtAuthentication.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
jwtAuthentication.AddAppServicesAuthentication();
....
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Dont be confused about the sloppy client certificate validation, this is still work in progress.
How do we need to change the code so we can have the two authentication modes along each other with a either-or way?
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);
});
});
}
I have created web api, when trying to add ssl get this error, when the code try to reach an endpoint in the web api i get this error, this is a self-signed certificates.
This is development environment, using visual studio 2019 to debug the code but no luck after trying to re-create the ssl certificates, checked guides about implement https in .net core apps, yet no luck.
Program.cs:
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000")
.UseSetting("https_port", "5001")
.UseKestrel(options =>
{
options.Listen(System.Net.IPAddress.Any, 5000);
options.Listen(System.Net.IPAddress.Any, 5001,
listenOptions => { listenOptions.UseHttps("localhost.pfx", "A2345_678b"); });
})
.UseStartup<Startup>();
}
ConfigureServices in Startup.cs:
services.AddSignalR();
services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<SmartContext>();
services.AddMvc(
options =>
{
options.SslPort = 5001;
options.Filters.Add(new RequireHttpsAttribute());
}
);
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
services.AddAntiforgery(
options =>
{
options.Cookie.Name = "_af";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.HeaderName = "X-XSRF-TOKEN";
}
);
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
});
services.AddDbContext<SmartContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
All you should need to do is run the following in your project root:
dotnet dev-certs https --trust
This should pop a dialog asking you if you want to add the cert to your trusted store, which you obviously should accept. You'll need to do this for each project. For example, it's not enough to trust your web app, if that web app is connecting to an API app. You need to do the same thing for the API app so that both certs are trusted.