Skip to content

Commit c3db22d

Browse files
committed
Add an optional client certs example
1 parent f908247 commit c3db22d

File tree

7 files changed

+191
-0
lines changed

7 files changed

+191
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
5+
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Reference Include="Microsoft.AspNetCore" />
10+
<Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
11+
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
12+
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
13+
<Reference Include="Microsoft.AspNetCore.Hosting" />
14+
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
15+
<Reference Include="Microsoft.Extensions.Hosting" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Net;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.AspNetCore.Server.Kestrel.Https;
4+
using Microsoft.Extensions.Hosting;
5+
6+
namespace Certificate.Sample
7+
{
8+
public class Program
9+
{
10+
public static void Main(string[] args)
11+
{
12+
CreateHostBuilder(args).Build().Run();
13+
}
14+
15+
public static IHostBuilder CreateHostBuilder(string[] args) =>
16+
Host.CreateDefaultBuilder(args)
17+
.ConfigureWebHostDefaults(webBuilder =>
18+
{
19+
webBuilder.UseStartup<Startup>();
20+
webBuilder.ConfigureKestrel((context, options) =>
21+
{
22+
// Kestrel can't have different ssl settings on the same IP because there's no way to change them based on SNI.
23+
// https://github.com/dotnet/runtime/issues/31097
24+
// TODO: Provide a certificate that allows 127.0.0.1 as a host so we don't have to worry about setting up DNS, host files, etc..
25+
options.Listen(IPAddress.Parse("127.0.0.1"), 5001, listenOptions =>
26+
{
27+
listenOptions.UseHttps(httpsOptions =>
28+
{
29+
httpsOptions.ClientCertificateMode = ClientCertificateMode.NoCertificate;
30+
});
31+
});
32+
options.Listen(IPAddress.Parse("127.0.0.2"), 5001, listenOptions =>
33+
{
34+
listenOptions.UseHttps(httpsOptions =>
35+
{
36+
httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
37+
});
38+
});
39+
});
40+
});
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "https://localhost:44331/",
7+
"sslPort": 44331
8+
}
9+
},
10+
"profiles": {
11+
"Certificate.Optional.Sample": {
12+
"commandName": "Project",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
},
17+
"applicationUrl": "https://127.0.0.1:5001/"
18+
}
19+
}
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Optional certificates sample
2+
============================
3+
4+
Client certificates are relatively easy to configure when they're required for requests, you configure it in the server bindings as required and add the auth handler to validate it. Things are much trickier when you only want to require client certifiates for some parts of your application.
5+
6+
Client certificates are not an HTTP feature, they're a TLS feature. As such they're not included in the HTTP request structure like headers, they're negotiated when establishing the connection. This makes it impossible to require a certificate for some requests but not others on a given connection.
7+
8+
There's an old way to renegotiate a connection if you find you need a client cert after it's established. It's a TLS action that pauses all traffic, redoes the TLS handshake, and allows you to request a client certificate. This caused a number of problems including weakening security, TCP deadlocks for POST requests, etc.. HTTP/2 has since disallowed this mechanism.
9+
10+
This example shows an pattern for requiring client certificates only in some parts of your site by using different host bindings. The application is set up using two host names, mydomain.com and cert.mydomain.com (I've cheated and used 127.0.0.1 and 127.0.0.2 here instead to avoid setting up DNS). cert.mydomain.com is configured in the server to require client certificates, but mydomain.com is not. When you request part of the site that requires a client certificate it can redirect to the cert.mydomain.com while preserving the request path and query and the client will prompt for a certificate.
11+
12+
Redirecting back to mydomain.com does not accomplish a real sign-out because the browser still caches the client cert selected for cert.mydomain.com. The only way to clear the browser cache is to close the browser.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Security.Claims;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Authentication.Certificate;
4+
using Microsoft.AspNetCore.Authorization;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.Extensions;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Hosting;
11+
using Microsoft.Net.Http.Headers;
12+
13+
namespace Certificate.Sample
14+
{
15+
public class Startup
16+
{
17+
// This method gets called by the runtime. Use this method to add services to the container.
18+
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
19+
public void ConfigureServices(IServiceCollection services)
20+
{
21+
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
22+
.AddCertificate(options =>
23+
{
24+
});
25+
26+
services.AddAuthorization();
27+
}
28+
29+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
30+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
31+
{
32+
app.UseRouting();
33+
34+
app.UseAuthentication();
35+
app.UseAuthorization();
36+
37+
app.UseEndpoints(endpoints =>
38+
{
39+
endpoints.Map("/required", context =>
40+
{
41+
if (context.User.Identity.IsAuthenticated)
42+
{
43+
return context.Response.WriteAsync($"Hello {context.User.Identity.Name} at {context.Request.Host}");
44+
}
45+
else
46+
{
47+
var request = context.Request;
48+
var redirect = UriHelper.BuildAbsolute("https", new HostString("127.0.0.2", context.Connection.LocalPort), request.PathBase, request.Path, request.QueryString);
49+
context.Response.Redirect(redirect, permanent: false, preserveMethod: true);
50+
return Task.CompletedTask;
51+
}
52+
});
53+
endpoints.Map("/signout", context =>
54+
{
55+
if (context.User.Identity.IsAuthenticated)
56+
{
57+
// Closing the connection doesn't reset Chrome's state, it still remembers which client cert was last used for which host until you close the browser.
58+
// context.Response.Headers[HeaderNames.Connection] = "close";
59+
60+
// Sign out by switching back to the other host. This isn't a real sign-out because the browser has still cached the client certificate for this host.
61+
// The only real way to clear that is to close the browser.
62+
if (context.Request.Host.Host.Equals("127.0.0.2"))
63+
{
64+
var request = context.Request;
65+
var redirect = UriHelper.BuildAbsolute("https", new HostString("127.0.0.1", context.Connection.LocalPort), request.PathBase);
66+
context.Response.Redirect(redirect, permanent: false, preserveMethod: true);
67+
}
68+
return context.Response.WriteAsync($"Goodbye {context.User.Identity.Name} at {context.Request.Host}");
69+
}
70+
else
71+
{
72+
return context.Response.WriteAsync("Already signed out.");
73+
}
74+
});
75+
endpoints.Map("{*url}", context =>
76+
{
77+
return context.Response.WriteAsync($"Hello {context.User.Identity.Name} at {context.Request.Host}");
78+
});
79+
});
80+
}
81+
}
82+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Debug",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
},
9+
"AllowedHosts": "*"
10+
}

src/Security/Security.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
164164
EndProject
165165
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.HttpSys", "..\Servers\HttpSys\src\Microsoft.AspNetCore.Server.HttpSys.csproj", "{D6C3C4A9-197B-47B5-8B72-35047CBC4F22}"
166166
EndProject
167+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Certificate.Optional.Sample", "Authentication\Certificate\samples\Certificate.Optional.Sample\Certificate.Optional.Sample.csproj", "{1B6960CF-0421-405A-B357-4CCC42255CA7}"
168+
EndProject
167169
Global
168170
GlobalSection(SolutionConfigurationPlatforms) = preSolution
169171
Debug|Any CPU = Debug|Any CPU
@@ -426,6 +428,10 @@ Global
426428
{D6C3C4A9-197B-47B5-8B72-35047CBC4F22}.Debug|Any CPU.Build.0 = Debug|Any CPU
427429
{D6C3C4A9-197B-47B5-8B72-35047CBC4F22}.Release|Any CPU.ActiveCfg = Release|Any CPU
428430
{D6C3C4A9-197B-47B5-8B72-35047CBC4F22}.Release|Any CPU.Build.0 = Release|Any CPU
431+
{1B6960CF-0421-405A-B357-4CCC42255CA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
432+
{1B6960CF-0421-405A-B357-4CCC42255CA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
433+
{1B6960CF-0421-405A-B357-4CCC42255CA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
434+
{1B6960CF-0421-405A-B357-4CCC42255CA7}.Release|Any CPU.Build.0 = Release|Any CPU
429435
EndGlobalSection
430436
GlobalSection(SolutionProperties) = preSolution
431437
HideSolutionNode = FALSE
@@ -507,6 +513,7 @@ Global
507513
{A665A1F8-D1A4-42AC-B8E9-71B6F57481D8} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
508514
{666AFB4D-68A5-4621-BB55-2CD82F0FB1F8} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
509515
{D6C3C4A9-197B-47B5-8B72-35047CBC4F22} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
516+
{1B6960CF-0421-405A-B357-4CCC42255CA7} = {4DF524BF-D9A9-46F2-882C-68C48FF5FF33}
510517
EndGlobalSection
511518
GlobalSection(ExtensibilityGlobals) = postSolution
512519
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}

0 commit comments

Comments
 (0)