Skip to content

Commit 3ac1314

Browse files
committed
Add the Challenge event
1 parent c3db22d commit 3ac1314

File tree

6 files changed

+82
-46
lines changed

6 files changed

+82
-46
lines changed

src/Security/Authentication/Certificate/samples/Certificate.Optional.Sample/Program.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ namespace Certificate.Sample
77
{
88
public class Program
99
{
10+
public const string Host1 = "127.0.0.1";
11+
public const string Host2 = "127.0.0.2";
12+
1013
public static void Main(string[] args)
1114
{
1215
CreateHostBuilder(args).Build().Run();
@@ -19,17 +22,16 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
1922
webBuilder.UseStartup<Startup>();
2023
webBuilder.ConfigureKestrel((context, options) =>
2124
{
22-
// Kestrel can't have different ssl settings on the same IP because there's no way to change them based on SNI.
25+
// Kestrel can't have different ssl settings for different hosts on the same IP because there's no way to change them based on SNI.
2326
// 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 =>
27+
options.Listen(IPAddress.Parse(Host1), 5001, listenOptions =>
2628
{
2729
listenOptions.UseHttps(httpsOptions =>
2830
{
2931
httpsOptions.ClientCertificateMode = ClientCertificateMode.NoCertificate;
3032
});
3133
});
32-
options.Listen(IPAddress.Parse("127.0.0.2"), 5001, listenOptions =>
34+
options.Listen(IPAddress.Parse(Host2), 5001, listenOptions =>
3335
{
3436
listenOptions.UseHttps(httpsOptions =>
3537
{

src/Security/Authentication/Certificate/samples/Certificate.Optional.Sample/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Optional certificates sample
22
============================
33

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.
4+
Client certificates are relatively easy to configure when they're required for all 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 certificates for some parts of your application.
55

66
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.
77

src/Security/Authentication/Certificate/samples/Certificate.Optional.Sample/Startup.cs

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
using System.Security.Claims;
21
using System.Threading.Tasks;
32
using Microsoft.AspNetCore.Authentication.Certificate;
4-
using Microsoft.AspNetCore.Authorization;
53
using Microsoft.AspNetCore.Builder;
64
using Microsoft.AspNetCore.Hosting;
75
using Microsoft.AspNetCore.Http;
86
using Microsoft.AspNetCore.Http.Extensions;
97
using Microsoft.Extensions.DependencyInjection;
10-
using Microsoft.Extensions.Hosting;
11-
using Microsoft.Net.Http.Headers;
128

139
namespace Certificate.Sample
1410
{
@@ -21,6 +17,20 @@ public void ConfigureServices(IServiceCollection services)
2117
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
2218
.AddCertificate(options =>
2319
{
20+
options.Events = new CertificateAuthenticationEvents()
21+
{
22+
// If there is no certificate we must be on Host1 that does not require one. Redirect to Host2 to prompt for a certificate.
23+
OnChallenge = context =>
24+
{
25+
var request = context.Request;
26+
var redirect = UriHelper.BuildAbsolute("https",
27+
new HostString(Program.Host2, context.HttpContext.Connection.LocalPort),
28+
request.PathBase, request.Path, request.QueryString);
29+
context.Response.Redirect(redirect, permanent: false, preserveMethod: true);
30+
context.HandleResponse(); // Don't do the default behavior that would send a 403 response.
31+
return Task.CompletedTask;
32+
}
33+
};
2434
});
2535

2636
services.AddAuthorization();
@@ -36,45 +46,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
3646

3747
app.UseEndpoints(endpoints =>
3848
{
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 =>
49+
endpoints.Map("/auth", context =>
5450
{
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";
51+
return context.Response.WriteAsync($"Hello {context.User.Identity.Name} at {context.Request.Host}");
52+
}).RequireAuthorization();
5953

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-
});
7554
endpoints.Map("{*url}", context =>
7655
{
77-
return context.Response.WriteAsync($"Hello {context.User.Identity.Name} at {context.Request.Host}");
56+
return context.Response.WriteAsync($"Hello {context.User.Identity.Name} at {context.Request.Host}. Try /auth");
7857
});
7958
});
8059
}

src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,19 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
130130
}
131131
}
132132

133-
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
133+
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
134134
{
135+
var authenticationChallengedContext = new CertificateChallengeContext(Context, Scheme, Options, properties);
136+
await Events.Challenge(authenticationChallengedContext);
137+
138+
if (authenticationChallengedContext.Handled)
139+
{
140+
return;
141+
}
142+
135143
// Certificate authentication takes place at the connection level. We can't prompt once we're in
136144
// user code, so the best thing to do is Forbid, not Challenge.
137-
return HandleForbiddenAsync(properties);
145+
await HandleForbiddenAsync(properties);
138146
}
139147

140148
private X509ChainPolicy BuildChainPolicy(X509Certificate2 certificate)

src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public class CertificateAuthenticationEvents
2828
/// </remarks>
2929
public Func<CertificateValidatedContext, Task> OnCertificateValidated { get; set; } = context => Task.CompletedTask;
3030

31+
/// <summary>
32+
/// Invoked before a challenge is sent back to the caller.
33+
/// </summary>
34+
public Func<CertificateChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask;
35+
3136
/// <summary>
3237
/// Invoked when a certificate fails authentication.
3338
/// </summary>
@@ -41,5 +46,10 @@ public class CertificateAuthenticationEvents
4146
/// <param name="context"></param>
4247
/// <returns></returns>
4348
public virtual Task CertificateValidated(CertificateValidatedContext context) => OnCertificateValidated(context);
49+
50+
/// <summary>
51+
/// Invoked before a challenge is sent back to the caller.
52+
/// </summary>
53+
public virtual Task Challenge(CertificateChallengeContext context) => OnChallenge(context);
4454
}
4555
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Authentication.Certificate
7+
{
8+
/// <summary>
9+
/// State for the Challenge event.
10+
/// </summary>
11+
public class CertificateChallengeContext : PropertiesContext<CertificateAuthenticationOptions>
12+
{
13+
/// <summary>
14+
/// Creates a new <see cref="CertificateChallengeContext"/>.
15+
/// </summary>
16+
/// <param name="context"></param>
17+
/// <param name="scheme"></param>
18+
/// <param name="options"></param>
19+
/// <param name="properties"></param>
20+
public CertificateChallengeContext(
21+
HttpContext context,
22+
AuthenticationScheme scheme,
23+
CertificateAuthenticationOptions options,
24+
AuthenticationProperties properties)
25+
: base(context, scheme, options, properties) { }
26+
27+
/// <summary>
28+
/// If true, will skip any default logic for this challenge.
29+
/// </summary>
30+
public bool Handled { get; private set; }
31+
32+
/// <summary>
33+
/// Skips any default logic for this challenge.
34+
/// </summary>
35+
public void HandleResponse() => Handled = true;
36+
}
37+
}

0 commit comments

Comments
 (0)