TFA cookies now validate security stamp (#1351)

This commit is contained in:
Hao Kung 2018-02-06 13:27:53 -08:00 committed by GitHub
parent 37b8abf841
commit c6a82ad19a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 373 additions and 106 deletions

View File

@ -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);
}

View File

@ -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
{ }
}

View File

@ -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>>();

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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");

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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)]

View File

@ -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))
{