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 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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue