TFA cookies now validate security stamp (#1351)
This commit is contained in:
parent
37b8abf841
commit
c6a82ad19a
|
|
@ -2147,6 +2147,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
await store.SetAuthenticatorKeyAsync(user, GenerateNewAuthenticatorKey(), CancellationToken);
|
||||
await UpdateSecurityStampInternal(user);
|
||||
return await UpdateAsync(user);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to validate the two factor remember client cookie security stamp.
|
||||
/// </summary>
|
||||
public interface ITwoFactorSecurityStampValidator : ISecurityStampValidator
|
||||
{ }
|
||||
}
|
||||
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
|
@ -69,7 +71,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
|
||||
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
|
||||
})
|
||||
.AddCookie(IdentityConstants.ApplicationScheme, o =>
|
||||
.AddCookie(IdentityConstants.ApplicationScheme, o =>
|
||||
{
|
||||
o.LoginPath = new PathString("/Account/Login");
|
||||
o.Events = new CookieAuthenticationEvents
|
||||
|
|
@ -77,13 +79,19 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
|
||||
};
|
||||
})
|
||||
.AddCookie(IdentityConstants.ExternalScheme, o =>
|
||||
.AddCookie(IdentityConstants.ExternalScheme, o =>
|
||||
{
|
||||
o.Cookie.Name = IdentityConstants.ExternalScheme;
|
||||
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
|
||||
})
|
||||
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme,
|
||||
o => o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme)
|
||||
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
|
||||
{
|
||||
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
|
||||
o.Events = new CookieAuthenticationEvents
|
||||
{
|
||||
OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
|
||||
};
|
||||
})
|
||||
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
|
||||
{
|
||||
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
|
||||
|
|
@ -101,6 +109,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// No interface for the error describer so we can add errors without rev'ing the interface
|
||||
services.TryAddScoped<IdentityErrorDescriber>();
|
||||
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
|
||||
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
|
||||
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
|
||||
services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>();
|
||||
services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
|
@ -16,10 +17,6 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
|
||||
public class SecurityStampValidator<TUser> : ISecurityStampValidator where TUser : class
|
||||
{
|
||||
private readonly SignInManager<TUser> _signInManager;
|
||||
private readonly SecurityStampValidatorOptions _options;
|
||||
private ISystemClock _clock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="SecurityStampValidator{TUser}"/>.
|
||||
/// </summary>
|
||||
|
|
@ -36,11 +33,62 @@ namespace Microsoft.AspNetCore.Identity
|
|||
{
|
||||
throw new ArgumentNullException(nameof(signInManager));
|
||||
}
|
||||
_signInManager = signInManager;
|
||||
_options = options.Value;
|
||||
_clock = clock;
|
||||
SignInManager = signInManager;
|
||||
Options = options.Value;
|
||||
Clock = clock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The SignInManager.
|
||||
/// </summary>
|
||||
public SignInManager<TUser> SignInManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SecurityStampValidatorOptions"/>.
|
||||
/// </summary>
|
||||
public SecurityStampValidatorOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemClock"/>.
|
||||
/// </summary>
|
||||
public ISystemClock Clock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the security stamp has been verified.
|
||||
/// </summary>
|
||||
/// <param name="user">The user who has been verified.</param>
|
||||
/// <param name="context">The <see cref="CookieValidatePrincipalContext"/>.</param>
|
||||
/// <returns>A task.</returns>
|
||||
protected virtual async Task SecurityStampVerified(TUser user, CookieValidatePrincipalContext context)
|
||||
{
|
||||
var newPrincipal = await SignInManager.CreateUserPrincipalAsync(user);
|
||||
|
||||
if (Options.OnRefreshingPrincipal != null)
|
||||
{
|
||||
var replaceContext = new SecurityStampRefreshingPrincipalContext
|
||||
{
|
||||
CurrentPrincipal = context.Principal,
|
||||
NewPrincipal = newPrincipal
|
||||
};
|
||||
|
||||
// Note: a null principal is allowed and results in a failed authentication.
|
||||
await Options.OnRefreshingPrincipal(replaceContext);
|
||||
newPrincipal = replaceContext.NewPrincipal;
|
||||
}
|
||||
|
||||
// REVIEW: note we lost login authentication method
|
||||
context.ReplacePrincipal(newPrincipal);
|
||||
context.ShouldRenew = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the principal's security stamp, returns the matching user if successful
|
||||
/// </summary>
|
||||
/// <param name="principal">The principal to verify.</param>
|
||||
/// <returns>The verified user or null if verification fails.</returns>
|
||||
protected virtual Task<TUser> VerifySecurityStamp(ClaimsPrincipal principal)
|
||||
=> SignInManager.ValidateSecurityStampAsync(principal);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a security stamp of an identity as an asynchronous operation, and rebuilds the identity if the validation succeeds, otherwise rejects
|
||||
/// the identity.
|
||||
|
|
@ -51,9 +99,9 @@ namespace Microsoft.AspNetCore.Identity
|
|||
public virtual async Task ValidateAsync(CookieValidatePrincipalContext context)
|
||||
{
|
||||
var currentUtc = DateTimeOffset.UtcNow;
|
||||
if (context.Options != null && _clock != null)
|
||||
if (context.Options != null && Clock != null)
|
||||
{
|
||||
currentUtc = _clock.UtcNow;
|
||||
currentUtc = Clock.UtcNow;
|
||||
}
|
||||
var issuedUtc = context.Properties.IssuedUtc;
|
||||
|
||||
|
|
@ -62,36 +110,19 @@ namespace Microsoft.AspNetCore.Identity
|
|||
if (issuedUtc != null)
|
||||
{
|
||||
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
|
||||
validate = timeElapsed > _options.ValidationInterval;
|
||||
validate = timeElapsed > Options.ValidationInterval;
|
||||
}
|
||||
if (validate)
|
||||
{
|
||||
var user = await _signInManager.ValidateSecurityStampAsync(context.Principal);
|
||||
var user = await VerifySecurityStamp(context.Principal);
|
||||
if (user != null)
|
||||
{
|
||||
var newPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
|
||||
|
||||
if (_options.OnRefreshingPrincipal != null)
|
||||
{
|
||||
var replaceContext = new SecurityStampRefreshingPrincipalContext
|
||||
{
|
||||
CurrentPrincipal = context.Principal,
|
||||
NewPrincipal = newPrincipal
|
||||
};
|
||||
|
||||
// Note: a null principal is allowed and results in a failed authentication.
|
||||
await _options.OnRefreshingPrincipal(replaceContext);
|
||||
newPrincipal = replaceContext.NewPrincipal;
|
||||
}
|
||||
|
||||
// REVIEW: note we lost login authentication method
|
||||
context.ReplacePrincipal(newPrincipal);
|
||||
context.ShouldRenew = true;
|
||||
await SecurityStampVerified(user, context);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.RejectPrincipal();
|
||||
await _signInManager.SignOutAsync();
|
||||
await SignInManager.SignOutAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,19 +136,30 @@ namespace Microsoft.AspNetCore.Identity
|
|||
{
|
||||
/// <summary>
|
||||
/// Validates a principal against a user's stored security stamp.
|
||||
/// the identity.
|
||||
/// </summary>
|
||||
/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/>
|
||||
/// and <see cref="Http.Authentication.AuthenticationProperties"/> to validate.</param>
|
||||
/// and <see cref="AuthenticationProperties"/> to validate.</param>
|
||||
/// <returns>The <see cref="Task"/> that represents the asynchronous validation operation.</returns>
|
||||
public static Task ValidatePrincipalAsync(CookieValidatePrincipalContext context)
|
||||
=> ValidateAsync<ISecurityStampValidator>(context);
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate the <see cref="IdentityConstants.TwoFactorUserIdScheme"/> and
|
||||
/// <see cref="IdentityConstants.TwoFactorRememberMeScheme"/> cookies against the user's
|
||||
/// stored security stamp.
|
||||
/// </summary>
|
||||
/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/>
|
||||
/// and <see cref="AuthenticationProperties"/> to validate.</param>
|
||||
/// <returns></returns>
|
||||
|
||||
public static Task ValidateAsync<TValidator>(CookieValidatePrincipalContext context) where TValidator : ISecurityStampValidator
|
||||
{
|
||||
if (context.HttpContext.RequestServices == null)
|
||||
{
|
||||
throw new InvalidOperationException("RequestServices is null.");
|
||||
}
|
||||
|
||||
var validator = context.HttpContext.RequestServices.GetRequiredService<ISecurityStampValidator>();
|
||||
var validator = context.HttpContext.RequestServices.GetRequiredService<TValidator>();
|
||||
return validator.ValidateAsync(context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,18 +220,46 @@ namespace Microsoft.AspNetCore.Identity
|
|||
return null;
|
||||
}
|
||||
var user = await UserManager.GetUserAsync(principal);
|
||||
if (user != null && UserManager.SupportsUserSecurityStamp)
|
||||
if (await ValidateSecurityStampAsync(user, principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType)))
|
||||
{
|
||||
var securityStamp =
|
||||
principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType);
|
||||
if (securityStamp == await UserManager.GetSecurityStampAsync(user))
|
||||
{
|
||||
return user;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the security stamp for the specified <paramref name="principal"/> from one of
|
||||
/// the two factor principals (remember client or user id) against
|
||||
/// the persisted stamp for the current user, as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="principal">The principal whose stamp should be validated.</param>
|
||||
/// <returns>The task object representing the asynchronous operation. The task will contain the <typeparamref name="TUser"/>
|
||||
/// if the stamp matches the persisted value, otherwise it will return false.</returns>
|
||||
public virtual async Task<TUser> ValidateTwoFactorSecurityStampAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
if (principal == null || principal.Identity?.Name == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var user = await UserManager.FindByIdAsync(principal.Identity.Name);
|
||||
if (await ValidateSecurityStampAsync(user, principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType)))
|
||||
{
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the security stamp for the specified <paramref name="user"/>. Will always return false
|
||||
/// if the userManager does not support security stamps.
|
||||
/// </summary>
|
||||
/// <param name="user">The user whose stamp should be validated.</param>
|
||||
/// <param name="securityStamp">The expected security stamp value.</param>
|
||||
/// <returns>True if the stamp matches the persisted value, otherwise it will return false.</returns>
|
||||
public virtual async Task<bool> ValidateSecurityStampAsync(TUser user, string securityStamp)
|
||||
=> user != null && UserManager.SupportsUserSecurityStamp
|
||||
&& securityStamp == await UserManager.GetSecurityStampAsync(user);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to sign in the specified <paramref name="user"/> and <paramref name="password"/> combination
|
||||
/// as an asynchronous operation.
|
||||
|
|
@ -343,11 +371,9 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
public virtual async Task RememberTwoFactorClientAsync(TUser user)
|
||||
{
|
||||
var userId = await UserManager.GetUserIdAsync(user);
|
||||
var rememberBrowserIdentity = new ClaimsIdentity(IdentityConstants.TwoFactorRememberMeScheme);
|
||||
rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId));
|
||||
var principal = await StoreRememberClient(user);
|
||||
await Context.SignInAsync(IdentityConstants.TwoFactorRememberMeScheme,
|
||||
new ClaimsPrincipal(rememberBrowserIdentity),
|
||||
principal,
|
||||
new AuthenticationProperties { IsPersistent = true });
|
||||
}
|
||||
|
||||
|
|
@ -655,6 +681,19 @@ namespace Microsoft.AspNetCore.Identity
|
|||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
|
||||
internal async Task<ClaimsPrincipal> StoreRememberClient(TUser user)
|
||||
{
|
||||
var userId = await UserManager.GetUserIdAsync(user);
|
||||
var rememberBrowserIdentity = new ClaimsIdentity(IdentityConstants.TwoFactorRememberMeScheme);
|
||||
rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId));
|
||||
if (UserManager.SupportsUserSecurityStamp)
|
||||
{
|
||||
var stamp = await UserManager.GetSecurityStampAsync(user);
|
||||
rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp));
|
||||
}
|
||||
return new ClaimsPrincipal(rememberBrowserIdentity);
|
||||
}
|
||||
|
||||
private ClaimsIdentity CreateIdentity(TwoFactorAuthenticationInfo info)
|
||||
{
|
||||
if (info == null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// 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.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for validation of two factor identity cookie security stamp.
|
||||
/// </summary>
|
||||
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
|
||||
public class TwoFactorSecurityStampValidator<TUser> : SecurityStampValidator<TUser>, ITwoFactorSecurityStampValidator where TUser : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="SecurityStampValidator{TUser}"/>.
|
||||
/// </summary>
|
||||
/// <param name="options">Used to access the <see cref="IdentityOptions"/>.</param>
|
||||
/// <param name="signInManager">The <see cref="SignInManager{TUser}"/>.</param>
|
||||
/// <param name="clock">The system clock.</param>
|
||||
public TwoFactorSecurityStampValidator(IOptions<SecurityStampValidatorOptions> options, SignInManager<TUser> signInManager, ISystemClock clock) : base(options, signInManager, clock)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the principal's security stamp, returns the matching user if successful
|
||||
/// </summary>
|
||||
/// <param name="principal">The principal to verify.</param>
|
||||
/// <returns>The verified user or null if verification fails.</returns>
|
||||
protected override Task<TUser> VerifySecurityStamp(ClaimsPrincipal principal)
|
||||
=> SignInManager.ValidateTwoFactorSecurityStampAsync(principal);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the security stamp has been verified.
|
||||
/// </summary>
|
||||
/// <param name="user">The user who has been verified.</param>
|
||||
/// <param name="context">The <see cref="CookieValidatePrincipalContext"/>.</param>
|
||||
/// <returns>A task.</returns>
|
||||
protected override Task SecurityStampVerified(TUser user, CookieValidatePrincipalContext context)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -290,7 +290,27 @@ namespace Microsoft.AspNetCore.Identity.Test
|
|||
var user = CreateTestUser(username, useNamePrefixAsUserName: true);
|
||||
var stamp = await manager.GetSecurityStampAsync(user);
|
||||
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
|
||||
Assert.NotNull(await manager.GetSecurityStampAsync(user));
|
||||
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test.
|
||||
/// </summary>
|
||||
/// <returns>Task</returns>
|
||||
[Fact]
|
||||
public async Task ResetAuthenticatorKeyUpdatesSecurityStamp()
|
||||
{
|
||||
if (ShouldSkipDbTests())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var manager = CreateManager();
|
||||
var username = "Create" + Guid.NewGuid().ToString();
|
||||
var user = CreateTestUser(username, useNamePrefixAsUserName: true);
|
||||
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
|
||||
var stamp = await manager.GetSecurityStampAsync(user);
|
||||
IdentityResultAssert.IsSuccess(await manager.ResetAuthenticatorKeyAsync(user));
|
||||
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -23,13 +23,16 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
|||
internal class ResetAuthenticatorModel<TUser> : ResetAuthenticatorModel where TUser : IdentityUser
|
||||
{
|
||||
UserManager<TUser> _userManager;
|
||||
private readonly SignInManager<TUser> _signInManager;
|
||||
ILogger<ResetAuthenticatorModel> _logger;
|
||||
|
||||
public ResetAuthenticatorModel(
|
||||
UserManager<TUser> userManager,
|
||||
SignInManager<TUser> signInManager,
|
||||
ILogger<ResetAuthenticatorModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +59,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
|||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
|
||||
|
||||
return RedirectToPage("./EnableAuthenticator");
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
|||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public virtual Task<IActionResult> OnGet() => throw new NotImplementedException();
|
||||
public virtual Task<IActionResult> OnGetAsync() => throw new NotImplementedException();
|
||||
|
||||
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
|
||||
|
||||
}
|
||||
|
||||
internal class TwoFactorAuthenticationModel<TUser> : TwoFactorAuthenticationModel where TUser : IdentityUser
|
||||
|
|
@ -41,7 +44,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<IActionResult> OnGet()
|
||||
public override async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
|
|
@ -57,7 +60,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
|||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
public override async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
|
|
|
|||
|
|
@ -72,40 +72,56 @@ namespace Microsoft.AspNetCore.Identity.Test
|
|||
public async Task OnValidatePrincipalTestSuccess(bool isPersistent)
|
||||
{
|
||||
var user = new TestUser("test");
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
|
||||
await RunApplicationCookieTest(user, httpContext, /*shouldStampValidate*/true, async () =>
|
||||
{
|
||||
var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
|
||||
id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
|
||||
var principal = new ClaimsPrincipal(id);
|
||||
var properties = new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1), IsPersistent = isPersistent };
|
||||
var ticket = new AuthenticationTicket(principal,
|
||||
properties,
|
||||
IdentityConstants.ApplicationScheme);
|
||||
|
||||
var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket);
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.NotNull(context.Options);
|
||||
Assert.NotNull(context.Principal);
|
||||
await SecurityStampValidator.ValidatePrincipalAsync(context);
|
||||
Assert.NotNull(context.Principal);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RunApplicationCookieTest(TestUser user, Mock<HttpContext> httpContext, bool shouldStampValidate, Func<Task> testCode)
|
||||
{
|
||||
var userManager = MockHelpers.MockUserManager<TestUser>();
|
||||
var claimsManager = new Mock<IUserClaimsPrincipalFactory<TestUser>>();
|
||||
var identityOptions = new Mock<IOptions<IdentityOptions>>();
|
||||
identityOptions.Setup(a => a.Value).Returns(new IdentityOptions());
|
||||
var options = new Mock<IOptions<SecurityStampValidatorOptions>>();
|
||||
options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero });
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var contextAccessor = new Mock<IHttpContextAccessor>();
|
||||
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
|
||||
var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
|
||||
id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
|
||||
var principal = new ClaimsPrincipal(id);
|
||||
|
||||
var properties = new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1), IsPersistent = isPersistent };
|
||||
var signInManager = new Mock<SignInManager<TestUser>>(userManager.Object,
|
||||
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
|
||||
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user).Verifiable();
|
||||
signInManager.Setup(s => s.CreateUserPrincipalAsync(user)).ReturnsAsync(principal).Verifiable();
|
||||
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(shouldStampValidate ? user : default(TestUser)).Verifiable();
|
||||
|
||||
if (shouldStampValidate)
|
||||
{
|
||||
var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
|
||||
id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
|
||||
var principal = new ClaimsPrincipal(id);
|
||||
signInManager.Setup(s => s.CreateUserPrincipalAsync(user)).ReturnsAsync(principal).Verifiable();
|
||||
}
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(options.Object);
|
||||
services.AddSingleton(signInManager.Object);
|
||||
services.AddSingleton<ISecurityStampValidator>(new SecurityStampValidator<TestUser>(options.Object, signInManager.Object, new SystemClock()));
|
||||
httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider());
|
||||
|
||||
var ticket = new AuthenticationTicket(principal,
|
||||
properties,
|
||||
IdentityConstants.ApplicationScheme);
|
||||
var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket);
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.NotNull(context.Options);
|
||||
Assert.NotNull(context.Principal);
|
||||
await
|
||||
SecurityStampValidator.ValidatePrincipalAsync(context);
|
||||
Assert.NotNull(context.Principal);
|
||||
await testCode.Invoke();
|
||||
signInManager.VerifyAll();
|
||||
}
|
||||
|
||||
|
|
@ -113,36 +129,23 @@ namespace Microsoft.AspNetCore.Identity.Test
|
|||
public async Task OnValidateIdentityRejectsWhenValidateSecurityStampFails()
|
||||
{
|
||||
var user = new TestUser("test");
|
||||
var userManager = MockHelpers.MockUserManager<TestUser>();
|
||||
var claimsManager = new Mock<IUserClaimsPrincipalFactory<TestUser>>();
|
||||
var identityOptions = new Mock<IOptions<IdentityOptions>>();
|
||||
identityOptions.Setup(a => a.Value).Returns(new IdentityOptions());
|
||||
var options = new Mock<IOptions<SecurityStampValidatorOptions>>();
|
||||
options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero });
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var contextAccessor = new Mock<IHttpContextAccessor>();
|
||||
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
|
||||
var signInManager = new Mock<SignInManager<TestUser>>(userManager.Object,
|
||||
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
|
||||
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(default(TestUser)).Verifiable();
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(options.Object);
|
||||
services.AddSingleton(signInManager.Object);
|
||||
services.AddSingleton<ISecurityStampValidator>(new SecurityStampValidator<TestUser>(options.Object, signInManager.Object, new SystemClock()));
|
||||
httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider());
|
||||
var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
|
||||
id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
|
||||
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(id),
|
||||
new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1) },
|
||||
IdentityConstants.ApplicationScheme);
|
||||
var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket);
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.NotNull(context.Options);
|
||||
Assert.NotNull(context.Principal);
|
||||
await SecurityStampValidator.ValidatePrincipalAsync(context);
|
||||
Assert.Null(context.Principal);
|
||||
signInManager.VerifyAll();
|
||||
await RunApplicationCookieTest(user, httpContext, /*shouldStampValidate*/false, async () =>
|
||||
{
|
||||
var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
|
||||
id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(id),
|
||||
new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1) },
|
||||
IdentityConstants.ApplicationScheme);
|
||||
|
||||
var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket);
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.NotNull(context.Options);
|
||||
Assert.NotNull(context.Principal);
|
||||
await SecurityStampValidator.ValidatePrincipalAsync(context);
|
||||
Assert.Null(context.Principal);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -216,5 +219,51 @@ namespace Microsoft.AspNetCore.Identity.Test
|
|||
await SecurityStampValidator.ValidatePrincipalAsync(context);
|
||||
Assert.NotNull(context.Principal);
|
||||
}
|
||||
|
||||
private async Task RunRememberClientCookieTest(bool shouldStampValidate, bool validationSuccess)
|
||||
{
|
||||
var user = new TestUser("test");
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var userManager = MockHelpers.MockUserManager<TestUser>();
|
||||
userManager.Setup(u => u.GetUserIdAsync(user)).ReturnsAsync(user.Id).Verifiable();
|
||||
var claimsManager = new Mock<IUserClaimsPrincipalFactory<TestUser>>();
|
||||
var identityOptions = new Mock<IOptions<IdentityOptions>>();
|
||||
identityOptions.Setup(a => a.Value).Returns(new IdentityOptions());
|
||||
var options = new Mock<IOptions<SecurityStampValidatorOptions>>();
|
||||
options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero });
|
||||
var contextAccessor = new Mock<IHttpContextAccessor>();
|
||||
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
|
||||
var signInManager = new Mock<SignInManager<TestUser>>(userManager.Object,
|
||||
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
|
||||
signInManager.Setup(s => s.ValidateTwoFactorSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(shouldStampValidate ? user : default).Verifiable();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(options.Object);
|
||||
services.AddSingleton(signInManager.Object);
|
||||
services.AddSingleton<ITwoFactorSecurityStampValidator>(new TwoFactorSecurityStampValidator<TestUser>(options.Object, signInManager.Object, new SystemClock()));
|
||||
httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider());
|
||||
|
||||
var principal = await signInManager.Object.StoreRememberClient(user);
|
||||
var ticket = new AuthenticationTicket(principal,
|
||||
new AuthenticationProperties { IsPersistent = true },
|
||||
IdentityConstants.TwoFactorRememberMeScheme);
|
||||
var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket);
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.NotNull(context.Options);
|
||||
Assert.NotNull(context.Principal);
|
||||
await SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>(context);
|
||||
Assert.Equal(validationSuccess, context.Principal != null);
|
||||
|
||||
signInManager.VerifyAll();
|
||||
userManager.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task TwoFactorRememberClientOnValidatePrincipalTestSuccess()
|
||||
=> RunRememberClientCookieTest(shouldStampValidate: true, validationSuccess: true);
|
||||
|
||||
[Fact]
|
||||
public Task TwoFactorRememberClientOnValidatePrincipalRejectsWhenValidateSecurityStampFails()
|
||||
=> RunRememberClientCookieTest(shouldStampValidate: false, validationSuccess: false);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ using System.Security.Claims;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -17,7 +16,7 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Identity.Test
|
||||
{
|
||||
public class SignManagerInTest
|
||||
public class SignInManagerTest
|
||||
{
|
||||
//[Theory]
|
||||
//[InlineData(true)]
|
||||
|
|
|
|||
|
|
@ -184,7 +184,8 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
|||
[Fact]
|
||||
public async Task TwoFactorRememberCookieVerification()
|
||||
{
|
||||
var server = CreateServer();
|
||||
var clock = new TestClock();
|
||||
var server = CreateServer(services => services.AddSingleton<ISystemClock>(clock));
|
||||
|
||||
var transaction1 = await SendAsync(server, "http://example.com/createMe");
|
||||
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
|
||||
|
|
@ -199,6 +200,46 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
|||
|
||||
var transaction3 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction3.Response.StatusCode);
|
||||
|
||||
// Wait for validation interval
|
||||
clock.Add(TimeSpan.FromMinutes(30));
|
||||
|
||||
var transaction4 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction4.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TwoFactorRememberCookieClearedBySecurityStampChange()
|
||||
{
|
||||
var clock = new TestClock();
|
||||
var server = CreateServer(services => services.AddSingleton<ISystemClock>(clock));
|
||||
|
||||
var transaction1 = await SendAsync(server, "http://example.com/createMe");
|
||||
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
|
||||
Assert.Null(transaction1.SetCookie);
|
||||
|
||||
var transaction2 = await SendAsync(server, "http://example.com/twofactorRememeber");
|
||||
Assert.Equal(HttpStatusCode.OK, transaction2.Response.StatusCode);
|
||||
|
||||
var setCookie = transaction2.SetCookie;
|
||||
Assert.Contains(IdentityConstants.TwoFactorRememberMeScheme + "=", setCookie);
|
||||
Assert.Contains("; expires=", setCookie);
|
||||
|
||||
var transaction3 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction3.Response.StatusCode);
|
||||
|
||||
var transaction4 = await SendAsync(server, "http://example.com/signoutEverywhere", transaction2.CookieNameValue);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction4.Response.StatusCode);
|
||||
|
||||
// Doesn't validate until after interval has passed
|
||||
var transaction5 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction5.Response.StatusCode);
|
||||
|
||||
// Wait for validation interval
|
||||
clock.Add(TimeSpan.FromMinutes(30));
|
||||
|
||||
var transaction6 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, transaction6.Response.StatusCode);
|
||||
}
|
||||
|
||||
private static string FindClaimValue(Transaction transaction, string claimType)
|
||||
|
|
@ -249,9 +290,11 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
|||
var result = await userManager.CreateAsync(new TestUser("simple"), "aaaaaa");
|
||||
res.StatusCode = result.Succeeded ? 200 : 500;
|
||||
}
|
||||
else if (req.Path == new PathString("/protected"))
|
||||
else if (req.Path == new PathString("/signoutEverywhere"))
|
||||
{
|
||||
res.StatusCode = 401;
|
||||
var user = await userManager.FindByNameAsync("hao");
|
||||
var result = await userManager.UpdateSecurityStampAsync(user);
|
||||
res.StatusCode = result.Succeeded ? 200 : 500;
|
||||
}
|
||||
else if (req.Path.StartsWithSegments(new PathString("/pwdLogin"), out remainder))
|
||||
{
|
||||
|
|
@ -271,8 +314,10 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
|||
var result = await signInManager.IsTwoFactorClientRememberedAsync(user);
|
||||
res.StatusCode = result ? 200 : 500;
|
||||
}
|
||||
else if (req.Path == new PathString("/twofactorSignIn"))
|
||||
else if (req.Path == new PathString("/hasTwoFactorUserId"))
|
||||
{
|
||||
var result = await context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme);
|
||||
res.StatusCode = result.Succeeded ? 200 : 500;
|
||||
}
|
||||
else if (req.Path == new PathString("/me"))
|
||||
{
|
||||
|
|
@ -295,7 +340,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
|||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddIdentity<TestUser, TestRole>();
|
||||
services.AddIdentity<TestUser, TestRole>().AddDefaultTokenProviders();
|
||||
services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>();
|
||||
services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>();
|
||||
configureServices?.Invoke(services);
|
||||
|
|
@ -346,7 +391,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
|||
};
|
||||
if (transaction.Response.Headers.Contains("Set-Cookie"))
|
||||
{
|
||||
transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault();
|
||||
transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").FirstOrDefault();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(transaction.SetCookie))
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue