Add certificate validation cache (#21847)
This commit is contained in:
parent
d15672bb8f
commit
8e4dadc0dd
|
|
@ -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 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.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, Microsoft.AspNetCore.Authentication.AuthenticationProperties properties) { 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, object> Parameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
public string RedirectUri { get { throw null; } set { } }
|
||||
public Microsoft.AspNetCore.Authentication.AuthenticationProperties Clone() { throw null; }
|
||||
protected bool? GetBool(string key) { throw null; }
|
||||
protected System.DateTimeOffset? GetDateTimeOffset(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 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.AuthenticationTicket Clone() { throw null; }
|
||||
}
|
||||
public partial class AuthenticationToken
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,6 +46,28 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
/// </summary>
|
||||
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>
|
||||
/// Indicates that authentication was successful.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
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>
|
||||
/// State values about the authentication session.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -52,5 +52,20 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
/// Additional state values for the authentication session.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,33 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test
|
|||
{
|
||||
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]
|
||||
public void DefaultConstructor_EmptyCollections()
|
||||
{
|
||||
|
|
@ -298,4 +325,4 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,8 @@ namespace Certificate.Sample
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
// Adding a ICertificateValidationCache will result in certificate auth caching the results, the default implementation uses a memory cache
|
||||
}).AddCertificateCache();
|
||||
|
||||
services.AddAuthorization();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,5 +87,23 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
string authenticationScheme,
|
||||
Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class
|
||||
=> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Security.Cryptography;
|
|||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
|
|||
internal class CertificateAuthenticationHandler : AuthenticationHandler<CertificateAuthenticationOptions>
|
||||
{
|
||||
private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2");
|
||||
private ICertificateValidationCache _cache;
|
||||
|
||||
public CertificateAuthenticationHandler(
|
||||
IOptionsMonitor<CertificateAuthenticationOptions> options,
|
||||
|
|
@ -41,11 +43,18 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
|
|||
/// <returns>A new instance of the events instance.</returns>
|
||||
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()
|
||||
{
|
||||
// You only get client certificates over HTTPS
|
||||
if (!Context.Request.IsHttps)
|
||||
{
|
||||
Logger.NotHttps();
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
|
|
@ -60,57 +69,21 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
|
|||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
// 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))
|
||||
if (_cache != null)
|
||||
{
|
||||
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)
|
||||
var cacheHit = _cache.Get(Context, clientCertificate);
|
||||
if (cacheHit != null)
|
||||
{
|
||||
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,
|
||||
Principal = CreatePrincipal(clientCertificate)
|
||||
};
|
||||
|
||||
await Events.CertificateValidated(certificateValidatedContext);
|
||||
|
||||
if (certificateValidatedContext.Result != null)
|
||||
{
|
||||
return certificateValidatedContext.Result;
|
||||
_cache.Put(Context, clientCertificate, result);
|
||||
}
|
||||
|
||||
certificateValidatedContext.Success();
|
||||
return certificateValidatedContext.Result;
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -120,7 +93,6 @@ namespace Microsoft.AspNetCore.Authentication.Certificate
|
|||
};
|
||||
|
||||
await Events.AuthenticationFailed(authenticationFailedContext);
|
||||
|
||||
if (authenticationFailedContext.Result != null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var authenticationChallengedContext = new CertificateChallengeContext(Context, Scheme, Options, properties);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ namespace Microsoft.Extensions.Logging
|
|||
internal static class LoggingExtensions
|
||||
{
|
||||
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> _certFailedValidation;
|
||||
|
||||
|
|
@ -28,6 +29,11 @@ namespace Microsoft.Extensions.Logging
|
|||
eventId: new EventId(2, "CertificateFailedValidation"),
|
||||
logLevel: LogLevel.Warning,
|
||||
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)
|
||||
|
|
@ -35,6 +41,11 @@ namespace Microsoft.Extensions.Logging
|
|||
_noCertificate(logger, null);
|
||||
}
|
||||
|
||||
public static void NotHttps(this ILogger logger)
|
||||
{
|
||||
_notHttps(logger, null);
|
||||
}
|
||||
|
||||
public static void CertificateRejected(this ILogger logger, string certificateType, string subject)
|
||||
{
|
||||
_certRejected(logger, certificateType, subject, null);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Authentication" />
|
||||
<Reference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
public async Task VerifyValidationEventPrincipalIsPropogated()
|
||||
{
|
||||
|
|
@ -526,7 +602,8 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
|
|||
Func<HttpContext, bool> handler = null,
|
||||
Uri baseAddress = null,
|
||||
bool wireUpHeaderMiddleware = false,
|
||||
string headerName = "")
|
||||
string headerName = "",
|
||||
bool useCache = false)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
|
|
@ -575,9 +652,10 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
|
|||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
AuthenticationBuilder authBuilder;
|
||||
if (configureOptions != null)
|
||||
{
|
||||
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
|
||||
authBuilder = services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
|
||||
{
|
||||
options.CustomTrustStore = configureOptions.CustomTrustStore;
|
||||
options.ChainTrustValidationMode = configureOptions.ChainTrustValidationMode;
|
||||
|
|
@ -591,7 +669,11 @@ namespace Microsoft.AspNetCore.Authentication.Certificate.Test
|
|||
}
|
||||
else
|
||||
{
|
||||
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
|
||||
authBuilder = services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
|
||||
}
|
||||
if (useCache)
|
||||
{
|
||||
authBuilder.AddCertificateCache();
|
||||
}
|
||||
|
||||
if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName))
|
||||
|
|
|
|||
Loading…
Reference in New Issue