Add certificate validation cache (#21847)

This commit is contained in:
Hao Kung 2020-06-05 00:58:47 -07:00 committed by GitHub
parent d15672bb8f
commit 8e4dadc0dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 414 additions and 50 deletions

View File

@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Authentication
public Microsoft.AspNetCore.Authentication.AuthenticationProperties Properties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] protected set { } } public Microsoft.AspNetCore.Authentication.AuthenticationProperties Properties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] protected set { } }
public bool Succeeded { get { throw null; } } public bool Succeeded { get { throw null; } }
public Microsoft.AspNetCore.Authentication.AuthenticationTicket Ticket { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] protected set { } } public Microsoft.AspNetCore.Authentication.AuthenticationTicket Ticket { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] protected set { } }
public Microsoft.AspNetCore.Authentication.AuthenticateResult Clone() { throw null; }
public static Microsoft.AspNetCore.Authentication.AuthenticateResult Fail(System.Exception failure) { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticateResult Fail(System.Exception failure) { throw null; }
public static Microsoft.AspNetCore.Authentication.AuthenticateResult Fail(System.Exception failure, Microsoft.AspNetCore.Authentication.AuthenticationProperties properties) { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticateResult Fail(System.Exception failure, Microsoft.AspNetCore.Authentication.AuthenticationProperties properties) { throw null; }
public static Microsoft.AspNetCore.Authentication.AuthenticateResult Fail(string failureMessage) { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticateResult Fail(string failureMessage) { throw null; }
@ -69,6 +70,7 @@ namespace Microsoft.AspNetCore.Authentication
public System.Collections.Generic.IDictionary<string, string> Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public System.Collections.Generic.IDictionary<string, string> Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public System.Collections.Generic.IDictionary<string, object> Parameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public System.Collections.Generic.IDictionary<string, object> Parameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string RedirectUri { get { throw null; } set { } } public string RedirectUri { get { throw null; } set { } }
public Microsoft.AspNetCore.Authentication.AuthenticationProperties Clone() { throw null; }
protected bool? GetBool(string key) { throw null; } protected bool? GetBool(string key) { throw null; }
protected System.DateTimeOffset? GetDateTimeOffset(string key) { throw null; } protected System.DateTimeOffset? GetDateTimeOffset(string key) { throw null; }
public T GetParameter<T>(string key) { throw null; } public T GetParameter<T>(string key) { throw null; }
@ -100,6 +102,7 @@ namespace Microsoft.AspNetCore.Authentication
public string AuthenticationScheme { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public string AuthenticationScheme { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public System.Security.Claims.ClaimsPrincipal Principal { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public System.Security.Claims.ClaimsPrincipal Principal { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public Microsoft.AspNetCore.Authentication.AuthenticationProperties Properties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public Microsoft.AspNetCore.Authentication.AuthenticationProperties Properties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public Microsoft.AspNetCore.Authentication.AuthenticationTicket Clone() { throw null; }
} }
public partial class AuthenticationToken public partial class AuthenticationToken
{ {

View File

@ -46,6 +46,28 @@ namespace Microsoft.AspNetCore.Authentication
/// </summary> /// </summary>
public bool None { get; protected set; } public bool None { get; protected set; }
/// <summary>
/// Create a new deep copy of the result
/// </summary>
/// <returns>A copy of the result</returns>
public AuthenticateResult Clone()
{
if (None)
{
return NoResult();
}
if (Failure != null)
{
return Fail(Failure, Properties.Clone());
}
if (Succeeded)
{
return Success(Ticket.Clone());
}
// This shouldn't happen
throw new NotImplementedException();
}
/// <summary> /// <summary>
/// Indicates that authentication was successful. /// Indicates that authentication was successful.
/// </summary> /// </summary>

View File

@ -45,6 +45,14 @@ namespace Microsoft.AspNetCore.Authentication
Parameters = parameters ?? new Dictionary<string, object>(StringComparer.Ordinal); Parameters = parameters ?? new Dictionary<string, object>(StringComparer.Ordinal);
} }
/// <summary>
/// Return a copy.
/// </summary>
/// <returns>A copy.</returns>
public AuthenticationProperties Clone()
=> new AuthenticationProperties(new Dictionary<string, string>(Items, StringComparer.Ordinal),
new Dictionary<string, object>(Parameters, StringComparer.Ordinal));
/// <summary> /// <summary>
/// State values about the authentication session. /// State values about the authentication session.
/// </summary> /// </summary>

View File

@ -52,5 +52,20 @@ namespace Microsoft.AspNetCore.Authentication
/// Additional state values for the authentication session. /// Additional state values for the authentication session.
/// </summary> /// </summary>
public AuthenticationProperties Properties { get; private set; } public AuthenticationProperties Properties { get; private set; }
/// <summary>
/// Returns a copy of the ticket.
/// Note: the claims principal will be cloned by calling Clone() on each of the Identities.
/// </summary>
/// <returns>A copy of the ticket</returns>
public AuthenticationTicket Clone()
{
var principal = new ClaimsPrincipal();
foreach (var identity in Principal.Identities)
{
principal.AddIdentity(identity.Clone());
}
return new AuthenticationTicket(principal, Properties.Clone(), AuthenticationScheme);
}
} }
} }

View File

@ -8,6 +8,33 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test
{ {
public class AuthenticationPropertiesTests public class AuthenticationPropertiesTests
{ {
[Fact]
public void Clone_Copies()
{
var items = new Dictionary<string, string>
{
["foo"] = "bar",
};
var value = "value";
var parameters = new Dictionary<string, object>
{
["foo2"] = value,
};
var props = new AuthenticationProperties(items, parameters);
Assert.Same(items, props.Items);
Assert.Same(parameters, props.Parameters);
var copy = props.Clone();
Assert.NotSame(props.Items, copy.Items);
Assert.NotSame(props.Parameters, copy.Parameters);
// Objects in the dictionaries will still be the same
Assert.Equal(props.Items, copy.Items);
Assert.Equal(props.Parameters, copy.Parameters);
props.Items["change"] = "good";
props.Parameters["something"] = "bad";
Assert.NotEqual(props.Items, copy.Items);
Assert.NotEqual(props.Parameters, copy.Parameters);
}
[Fact] [Fact]
public void DefaultConstructor_EmptyCollections() public void DefaultConstructor_EmptyCollections()
{ {
@ -298,4 +325,4 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test
} }
} }
} }
} }

View File

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Security.Claims;
using Xunit;
namespace Microsoft.AspNetCore.Authentication.Core.Test
{
public class AuthenticationTicketTests
{
[Fact]
public void Clone_Copies()
{
var items = new Dictionary<string, string>
{
["foo"] = "bar",
};
var value = "value";
var parameters = new Dictionary<string, object>
{
["foo2"] = value,
};
var props = new AuthenticationProperties(items, parameters);
var identity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, props, "scheme");
Assert.Same(items, ticket.Properties.Items);
Assert.Same(parameters, ticket.Properties.Parameters);
var copy = ticket.Clone();
Assert.NotSame(ticket.Principal, copy.Principal);
Assert.NotSame(ticket.Properties.Items, copy.Properties.Items);
Assert.NotSame(ticket.Properties.Parameters, copy.Properties.Parameters);
// Objects in the dictionaries will still be the same
Assert.Equal(ticket.Properties.Items, copy.Properties.Items);
Assert.Equal(ticket.Properties.Parameters, copy.Properties.Parameters);
props.Items["change"] = "good";
props.Parameters["something"] = "bad";
Assert.NotEqual(ticket.Properties.Items, copy.Properties.Items);
Assert.NotEqual(ticket.Properties.Parameters, copy.Properties.Parameters);
identity.AddClaim(new Claim("name", "value"));
Assert.True(ticket.Principal.HasClaim("name", "value"));
Assert.False(copy.Principal.HasClaim("name", "value"));
}
}
}

View File

@ -35,7 +35,8 @@ namespace Certificate.Sample
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
}); // Adding a ICertificateValidationCache will result in certificate auth caching the results, the default implementation uses a memory cache
}).AddCertificateCache();
services.AddAuthorization(); services.AddAuthorization();
} }

View File

@ -87,5 +87,23 @@ namespace Microsoft.Extensions.DependencyInjection
string authenticationScheme, string authenticationScheme,
Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class
=> builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler, TService>(authenticationScheme, configureOptions); => builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler, TService>(authenticationScheme, configureOptions);
/// <summary>
/// Adds certificate authentication.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <param name="configureOptions"></param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddCertificateCache(
this AuthenticationBuilder builder,
Action<CertificateValidationCacheOptions> configureOptions = null)
{
builder.Services.AddSingleton<ICertificateValidationCache, CertificateValidationCache>();
if (configureOptions != null)
{
builder.Services.Configure(configureOptions);
}
return builder;
}
} }
} }

View File

@ -8,6 +8,7 @@ using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
internal class CertificateAuthenticationHandler : AuthenticationHandler<CertificateAuthenticationOptions> internal class CertificateAuthenticationHandler : AuthenticationHandler<CertificateAuthenticationOptions>
{ {
private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2"); private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2");
private ICertificateValidationCache _cache;
public CertificateAuthenticationHandler( public CertificateAuthenticationHandler(
IOptionsMonitor<CertificateAuthenticationOptions> options, IOptionsMonitor<CertificateAuthenticationOptions> options,
@ -41,11 +43,18 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
/// <returns>A new instance of the events instance.</returns> /// <returns>A new instance of the events instance.</returns>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CertificateAuthenticationEvents()); protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CertificateAuthenticationEvents());
protected override Task InitializeHandlerAsync()
{
_cache = Context.RequestServices.GetService<ICertificateValidationCache>();
return base.InitializeHandlerAsync();
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{ {
// You only get client certificates over HTTPS // You only get client certificates over HTTPS
if (!Context.Request.IsHttps) if (!Context.Request.IsHttps)
{ {
Logger.NotHttps();
return AuthenticateResult.NoResult(); return AuthenticateResult.NoResult();
} }
@ -60,57 +69,21 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
return AuthenticateResult.NoResult(); return AuthenticateResult.NoResult();
} }
// If we have a self signed cert, and they're not allowed, exit early and not bother with if (_cache != null)
// any other validations.
if (clientCertificate.IsSelfSigned() &&
!Options.AllowedCertificateTypes.HasFlag(CertificateTypes.SelfSigned))
{ {
Logger.CertificateRejected("Self signed", clientCertificate.Subject); var cacheHit = _cache.Get(Context, clientCertificate);
return AuthenticateResult.Fail("Options do not allow self signed certificates."); if (cacheHit != null)
}
// If we have a chained cert, and they're not allowed, exit early and not bother with
// any other validations.
if (!clientCertificate.IsSelfSigned() &&
!Options.AllowedCertificateTypes.HasFlag(CertificateTypes.Chained))
{
Logger.CertificateRejected("Chained", clientCertificate.Subject);
return AuthenticateResult.Fail("Options do not allow chained certificates.");
}
var chainPolicy = BuildChainPolicy(clientCertificate);
var chain = new X509Chain
{
ChainPolicy = chainPolicy
};
var certificateIsValid = chain.Build(clientCertificate);
if (!certificateIsValid)
{
var chainErrors = new List<string>();
foreach (var validationFailure in chain.ChainStatus)
{ {
chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}"); return cacheHit;
} }
Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors);
return AuthenticateResult.Fail("Client certificate failed validation.");
} }
var certificateValidatedContext = new CertificateValidatedContext(Context, Scheme, Options) var result = await ValidateCertificateAsync(clientCertificate);
if (_cache != null)
{ {
ClientCertificate = clientCertificate, _cache.Put(Context, clientCertificate, result);
Principal = CreatePrincipal(clientCertificate)
};
await Events.CertificateValidated(certificateValidatedContext);
if (certificateValidatedContext.Result != null)
{
return certificateValidatedContext.Result;
} }
return result;
certificateValidatedContext.Success();
return certificateValidatedContext.Result;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -120,7 +93,6 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
}; };
await Events.AuthenticationFailed(authenticationFailedContext); await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null) if (authenticationFailedContext.Result != null)
{ {
return authenticationFailedContext.Result; return authenticationFailedContext.Result;
@ -130,6 +102,61 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
} }
} }
private async Task<AuthenticateResult> ValidateCertificateAsync(X509Certificate2 clientCertificate)
{
// If we have a self signed cert, and they're not allowed, exit early and not bother with
// any other validations.
if (clientCertificate.IsSelfSigned() &&
!Options.AllowedCertificateTypes.HasFlag(CertificateTypes.SelfSigned))
{
Logger.CertificateRejected("Self signed", clientCertificate.Subject);
return AuthenticateResult.Fail("Options do not allow self signed certificates.");
}
// If we have a chained cert, and they're not allowed, exit early and not bother with
// any other validations.
if (!clientCertificate.IsSelfSigned() &&
!Options.AllowedCertificateTypes.HasFlag(CertificateTypes.Chained))
{
Logger.CertificateRejected("Chained", clientCertificate.Subject);
return AuthenticateResult.Fail("Options do not allow chained certificates.");
}
var chainPolicy = BuildChainPolicy(clientCertificate);
var chain = new X509Chain
{
ChainPolicy = chainPolicy
};
var certificateIsValid = chain.Build(clientCertificate);
if (!certificateIsValid)
{
var chainErrors = new List<string>();
foreach (var validationFailure in chain.ChainStatus)
{
chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}");
}
Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors);
return AuthenticateResult.Fail("Client certificate failed validation.");
}
var certificateValidatedContext = new CertificateValidatedContext(Context, Scheme, Options)
{
ClientCertificate = clientCertificate,
Principal = CreatePrincipal(clientCertificate)
};
await Events.CertificateValidated(certificateValidatedContext);
if (certificateValidatedContext.Result != null)
{
return certificateValidatedContext.Result;
}
certificateValidatedContext.Success();
return certificateValidatedContext.Result;
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{ {
var authenticationChallengedContext = new CertificateChallengeContext(Context, Scheme, Options, properties); var authenticationChallengedContext = new CertificateChallengeContext(Context, Scheme, Options, properties);

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// MemoryCache based implementation used to store <see cref="AuthenticateResult"/> results after the certificate has been validated
/// </summary>
public class CertificateValidationCache : ICertificateValidationCache
{
private MemoryCache _cache;
private CertificateValidationCacheOptions _options;
public CertificateValidationCache(IOptions<CertificateValidationCacheOptions> options)
{
_options = options.Value;
_cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = _options.CacheSize });
}
/// <summary>
/// Get the <see cref="AuthenticateResult"/> for the connection and certificate.
/// </summary>
/// <param name="context">The HttpContext.</param>
/// <param name="certificate">The certificate.</param>
/// <returns>the <see cref="AuthenticateResult"/></returns>
public AuthenticateResult Get(HttpContext context, X509Certificate2 certificate)
=> _cache.Get<AuthenticateResult>(ComputeKey(certificate))?.Clone();
/// <summary>
/// Store a <see cref="AuthenticateResult"/> for the connection and certificate
/// </summary>
/// <param name="context">The HttpContext.</param>
/// <param name="certificate">The certificate.</param>
/// <param name="result">the <see cref="AuthenticateResult"/></param>
public void Put(HttpContext context, X509Certificate2 certificate, AuthenticateResult result)
=> _cache.Set(ComputeKey(certificate), result.Clone(), new MemoryCacheEntryOptions()
.SetSize(1).SetSlidingExpiration(_options.CacheEntryExpiration).SetAbsoluteExpiration(certificate.NotAfter));
private string ComputeKey(X509Certificate2 certificate)
=> certificate.GetCertHashString(HashAlgorithmName.SHA256);
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Configuration options for <see cref="CertificateValidationCache"/>
/// </summary>
public class CertificateValidationCacheOptions
{
/// <summary>
/// The expiration that should be used for entries in the MemoryCache, defaults to 2 minutes.
/// This is a sliding expiration that will extend each time the certificate is used, so long as the certificate is valid (see X509Certificate2.NotAfter).
/// </summary>
public TimeSpan CacheEntryExpiration { get; set; } = TimeSpan.FromMinutes(2);
/// <summary>
/// How many validated certificate results to store in the cache, defaults to 1024.
/// </summary>
public int CacheSize { get; set; } = 1024;
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Cache used to store <see cref="AuthenticateResult"/> results after the certificate has been validated
/// </summary>
public interface ICertificateValidationCache
{
/// <summary>
/// Get the <see cref="AuthenticateResult"/> for the connection and certificate.
/// </summary>
/// <param name="context">The HttpContext.</param>
/// <param name="certificate">The certificate.</param>
/// <returns>the <see cref="AuthenticateResult"/></returns>
AuthenticateResult Get(HttpContext context, X509Certificate2 certificate);
/// <summary>
/// Store a <see cref="AuthenticateResult"/> for the connection and certificate
/// </summary>
/// <param name="context">The HttpContext.</param>
/// <param name="certificate">The certificate.</param>
/// <param name="result">the <see cref="AuthenticateResult"/></param>
void Put(HttpContext context, X509Certificate2 certificate, AuthenticateResult result);
}
}

View File

@ -9,6 +9,7 @@ namespace Microsoft.Extensions.Logging
internal static class LoggingExtensions internal static class LoggingExtensions
{ {
private static Action<ILogger, Exception> _noCertificate; private static Action<ILogger, Exception> _noCertificate;
private static Action<ILogger, Exception> _notHttps;
private static Action<ILogger, string, string, Exception> _certRejected; private static Action<ILogger, string, string, Exception> _certRejected;
private static Action<ILogger, string, string, Exception> _certFailedValidation; private static Action<ILogger, string, string, Exception> _certFailedValidation;
@ -28,6 +29,11 @@ namespace Microsoft.Extensions.Logging
eventId: new EventId(2, "CertificateFailedValidation"), eventId: new EventId(2, "CertificateFailedValidation"),
logLevel: LogLevel.Warning, logLevel: LogLevel.Warning,
formatString: "Certificate validation failed, subject was {Subject}." + Environment.NewLine + "{ChainErrors}"); formatString: "Certificate validation failed, subject was {Subject}." + Environment.NewLine + "{ChainErrors}");
_notHttps = LoggerMessage.Define(
eventId: new EventId(3, "NotHttps"),
logLevel: LogLevel.Debug,
formatString: "Not https, skipping certificate authentication.");
} }
public static void NoCertificate(this ILogger logger) public static void NoCertificate(this ILogger logger)
@ -35,6 +41,11 @@ namespace Microsoft.Extensions.Logging
_noCertificate(logger, null); _noCertificate(logger, null);
} }
public static void NotHttps(this ILogger logger)
{
_notHttps(logger, null);
}
public static void CertificateRejected(this ILogger logger, string certificateType, string subject) public static void CertificateRejected(this ILogger logger, string certificateType, string subject)
{ {
_certRejected(logger, certificateType, subject, null); _certRejected(logger, certificateType, subject, null);

View File

@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication" /> <Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.Extensions.Caching.Memory" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -473,6 +473,82 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
} }
} }
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task VerifyValidationResultCanBeCached(bool cache)
{
const string Expected = "John Doe";
var validationCount = 0;
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
validationCount++;
// Make sure we get the validated principal
Assert.NotNull(context.Principal);
var claims = new[]
{
new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim("ValidationCount", validationCount.ToString(), ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
}
},
Certificates.SelfSignedValidWithNoEku, null, null, false, "", cache);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
XElement responseAsXml = null;
if (response.Content != null &&
response.Content.Headers.ContentType != null &&
response.Content.Headers.ContentType.MediaType == "text/xml")
{
var responseContent = await response.Content.ReadAsStringAsync();
responseAsXml = XElement.Parse(responseContent);
}
Assert.NotNull(responseAsXml);
var name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
Assert.Single(name);
Assert.Equal(Expected, name.First().Value);
var count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
Assert.Single(count);
Assert.Equal("1", count.First().Value);
// Second request should not trigger validation if caching
response = await server.CreateClient().GetAsync("https://example.com/");
responseAsXml = null;
if (response.Content != null &&
response.Content.Headers.ContentType != null &&
response.Content.Headers.ContentType.MediaType == "text/xml")
{
var responseContent = await response.Content.ReadAsStringAsync();
responseAsXml = XElement.Parse(responseContent);
}
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
Assert.Single(name);
Assert.Equal(Expected, name.First().Value);
count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
Assert.Single(count);
var expected = cache ? "1" : "2";
Assert.Equal(expected, count.First().Value);
}
[Fact] [Fact]
public async Task VerifyValidationEventPrincipalIsPropogated() public async Task VerifyValidationEventPrincipalIsPropogated()
{ {
@ -526,7 +602,8 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
Func<HttpContext, bool> handler = null, Func<HttpContext, bool> handler = null,
Uri baseAddress = null, Uri baseAddress = null,
bool wireUpHeaderMiddleware = false, bool wireUpHeaderMiddleware = false,
string headerName = "") string headerName = "",
bool useCache = false)
{ {
var builder = new WebHostBuilder() var builder = new WebHostBuilder()
.Configure(app => .Configure(app =>
@ -575,9 +652,10 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
}) })
.ConfigureServices(services => .ConfigureServices(services =>
{ {
AuthenticationBuilder authBuilder;
if (configureOptions != null) if (configureOptions != null)
{ {
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options => authBuilder = services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
{ {
options.CustomTrustStore = configureOptions.CustomTrustStore; options.CustomTrustStore = configureOptions.CustomTrustStore;
options.ChainTrustValidationMode = configureOptions.ChainTrustValidationMode; options.ChainTrustValidationMode = configureOptions.ChainTrustValidationMode;
@ -591,7 +669,11 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
} }
else else
{ {
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(); authBuilder = services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
}
if (useCache)
{
authBuilder.AddCertificateCache();
} }
if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName)) if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName))