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)); throw new ArgumentNullException(nameof(user));
} }
await store.SetAuthenticatorKeyAsync(user, GenerateNewAuthenticatorKey(), CancellationToken); await store.SetAuthenticatorKeyAsync(user, GenerateNewAuthenticatorKey(), CancellationToken);
await UpdateSecurityStampInternal(user);
return await UpdateAsync(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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -69,7 +71,7 @@ namespace Microsoft.Extensions.DependencyInjection
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme; options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
}) })
.AddCookie(IdentityConstants.ApplicationScheme, o => .AddCookie(IdentityConstants.ApplicationScheme, o =>
{ {
o.LoginPath = new PathString("/Account/Login"); o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents o.Events = new CookieAuthenticationEvents
@ -77,13 +79,19 @@ namespace Microsoft.Extensions.DependencyInjection
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
}; };
}) })
.AddCookie(IdentityConstants.ExternalScheme, o => .AddCookie(IdentityConstants.ExternalScheme, o =>
{ {
o.Cookie.Name = IdentityConstants.ExternalScheme; o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5); o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
}) })
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
o => o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme) {
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
};
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o => .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{ {
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme; 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 // No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>(); services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>(); services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>(); services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>(); services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>, SignInManager<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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
@ -16,10 +17,6 @@ namespace Microsoft.AspNetCore.Identity
/// <typeparam name="TUser">The type encapsulating a user.</typeparam> /// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public class SecurityStampValidator<TUser> : ISecurityStampValidator where TUser : class public class SecurityStampValidator<TUser> : ISecurityStampValidator where TUser : class
{ {
private readonly SignInManager<TUser> _signInManager;
private readonly SecurityStampValidatorOptions _options;
private ISystemClock _clock;
/// <summary> /// <summary>
/// Creates a new instance of <see cref="SecurityStampValidator{TUser}"/>. /// Creates a new instance of <see cref="SecurityStampValidator{TUser}"/>.
/// </summary> /// </summary>
@ -36,11 +33,62 @@ namespace Microsoft.AspNetCore.Identity
{ {
throw new ArgumentNullException(nameof(signInManager)); throw new ArgumentNullException(nameof(signInManager));
} }
_signInManager = signInManager; SignInManager = signInManager;
_options = options.Value; Options = options.Value;
_clock = clock; 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> /// <summary>
/// Validates a security stamp of an identity as an asynchronous operation, and rebuilds the identity if the validation succeeds, otherwise rejects /// Validates a security stamp of an identity as an asynchronous operation, and rebuilds the identity if the validation succeeds, otherwise rejects
/// the identity. /// the identity.
@ -51,9 +99,9 @@ namespace Microsoft.AspNetCore.Identity
public virtual async Task ValidateAsync(CookieValidatePrincipalContext context) public virtual async Task ValidateAsync(CookieValidatePrincipalContext context)
{ {
var currentUtc = DateTimeOffset.UtcNow; 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; var issuedUtc = context.Properties.IssuedUtc;
@ -62,36 +110,19 @@ namespace Microsoft.AspNetCore.Identity
if (issuedUtc != null) if (issuedUtc != null)
{ {
var timeElapsed = currentUtc.Subtract(issuedUtc.Value); var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > _options.ValidationInterval; validate = timeElapsed > Options.ValidationInterval;
} }
if (validate) if (validate)
{ {
var user = await _signInManager.ValidateSecurityStampAsync(context.Principal); var user = await VerifySecurityStamp(context.Principal);
if (user != null) if (user != null)
{ {
var newPrincipal = await _signInManager.CreateUserPrincipalAsync(user); await SecurityStampVerified(user, context);
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;
} }
else else
{ {
context.RejectPrincipal(); context.RejectPrincipal();
await _signInManager.SignOutAsync(); await SignInManager.SignOutAsync();
} }
} }
} }
@ -105,19 +136,30 @@ namespace Microsoft.AspNetCore.Identity
{ {
/// <summary> /// <summary>
/// Validates a principal against a user's stored security stamp. /// Validates a principal against a user's stored security stamp.
/// the identity.
/// </summary> /// </summary>
/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/> /// <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> /// <returns>The <see cref="Task"/> that represents the asynchronous validation operation.</returns>
public static Task ValidatePrincipalAsync(CookieValidatePrincipalContext context) 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) if (context.HttpContext.RequestServices == null)
{ {
throw new InvalidOperationException("RequestServices is 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); return validator.ValidateAsync(context);
} }
} }

View File

@ -220,18 +220,46 @@ namespace Microsoft.AspNetCore.Identity
return null; return null;
} }
var user = await UserManager.GetUserAsync(principal); var user = await UserManager.GetUserAsync(principal);
if (user != null && UserManager.SupportsUserSecurityStamp) if (await ValidateSecurityStampAsync(user, principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType)))
{ {
var securityStamp = return user;
principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType);
if (securityStamp == await UserManager.GetSecurityStampAsync(user))
{
return user;
}
} }
return null; 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> /// <summary>
/// Attempts to sign in the specified <paramref name="user"/> and <paramref name="password"/> combination /// Attempts to sign in the specified <paramref name="user"/> and <paramref name="password"/> combination
/// as an asynchronous operation. /// as an asynchronous operation.
@ -343,11 +371,9 @@ namespace Microsoft.AspNetCore.Identity
/// <returns>The task object representing the asynchronous operation.</returns> /// <returns>The task object representing the asynchronous operation.</returns>
public virtual async Task RememberTwoFactorClientAsync(TUser user) public virtual async Task RememberTwoFactorClientAsync(TUser user)
{ {
var userId = await UserManager.GetUserIdAsync(user); var principal = await StoreRememberClient(user);
var rememberBrowserIdentity = new ClaimsIdentity(IdentityConstants.TwoFactorRememberMeScheme);
rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId));
await Context.SignInAsync(IdentityConstants.TwoFactorRememberMeScheme, await Context.SignInAsync(IdentityConstants.TwoFactorRememberMeScheme,
new ClaimsPrincipal(rememberBrowserIdentity), principal,
new AuthenticationProperties { IsPersistent = true }); new AuthenticationProperties { IsPersistent = true });
} }
@ -655,6 +681,19 @@ namespace Microsoft.AspNetCore.Identity
return new ClaimsPrincipal(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) private ClaimsIdentity CreateIdentity(TwoFactorAuthenticationInfo info)
{ {
if (info == null) 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 user = CreateTestUser(username, useNamePrefixAsUserName: true);
var stamp = await manager.GetSecurityStampAsync(user); var stamp = await manager.GetSecurityStampAsync(user);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(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> /// <summary>

View File

@ -23,13 +23,16 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
internal class ResetAuthenticatorModel<TUser> : ResetAuthenticatorModel where TUser : IdentityUser internal class ResetAuthenticatorModel<TUser> : ResetAuthenticatorModel where TUser : IdentityUser
{ {
UserManager<TUser> _userManager; UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
ILogger<ResetAuthenticatorModel> _logger; ILogger<ResetAuthenticatorModel> _logger;
public ResetAuthenticatorModel( public ResetAuthenticatorModel(
UserManager<TUser> userManager, UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
ILogger<ResetAuthenticatorModel> logger) ILogger<ResetAuthenticatorModel> logger)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager;
_logger = logger; _logger = logger;
} }
@ -56,6 +59,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
await _userManager.ResetAuthenticatorKeyAsync(user); await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); _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."; StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
return RedirectToPage("./EnableAuthenticator"); return RedirectToPage("./EnableAuthenticator");

View File

@ -24,7 +24,10 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
[TempData] [TempData]
public string StatusMessage { get; set; } 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 internal class TwoFactorAuthenticationModel<TUser> : TwoFactorAuthenticationModel where TUser : IdentityUser
@ -41,7 +44,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
_logger = logger; _logger = logger;
} }
public override async Task<IActionResult> OnGet() public override async Task<IActionResult> OnGetAsync()
{ {
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
if (user == null) if (user == null)
@ -57,7 +60,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
return Page(); return Page();
} }
public async Task<IActionResult> OnPost() public override async Task<IActionResult> OnPostAsync()
{ {
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
if (user == null) if (user == null)

View File

@ -72,40 +72,56 @@ namespace Microsoft.AspNetCore.Identity.Test
public async Task OnValidatePrincipalTestSuccess(bool isPersistent) public async Task OnValidatePrincipalTestSuccess(bool isPersistent)
{ {
var user = new TestUser("test"); 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 userManager = MockHelpers.MockUserManager<TestUser>();
var claimsManager = new Mock<IUserClaimsPrincipalFactory<TestUser>>(); var claimsManager = new Mock<IUserClaimsPrincipalFactory<TestUser>>();
var identityOptions = new Mock<IOptions<IdentityOptions>>(); var identityOptions = new Mock<IOptions<IdentityOptions>>();
identityOptions.Setup(a => a.Value).Returns(new IdentityOptions()); identityOptions.Setup(a => a.Value).Returns(new IdentityOptions());
var options = new Mock<IOptions<SecurityStampValidatorOptions>>(); var options = new Mock<IOptions<SecurityStampValidatorOptions>>();
options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero }); options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero });
var httpContext = new Mock<HttpContext>();
var contextAccessor = new Mock<IHttpContextAccessor>(); var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object); 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, var signInManager = new Mock<SignInManager<TestUser>>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().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.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(shouldStampValidate ? user : default(TestUser)).Verifiable();
signInManager.Setup(s => s.CreateUserPrincipalAsync(user)).ReturnsAsync(principal).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(); var services = new ServiceCollection();
services.AddSingleton(options.Object); services.AddSingleton(options.Object);
services.AddSingleton(signInManager.Object); services.AddSingleton(signInManager.Object);
services.AddSingleton<ISecurityStampValidator>(new SecurityStampValidator<TestUser>(options.Object, signInManager.Object, new SystemClock())); services.AddSingleton<ISecurityStampValidator>(new SecurityStampValidator<TestUser>(options.Object, signInManager.Object, new SystemClock()));
httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider());
var ticket = new AuthenticationTicket(principal, await testCode.Invoke();
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);
signInManager.VerifyAll(); signInManager.VerifyAll();
} }
@ -113,36 +129,23 @@ namespace Microsoft.AspNetCore.Identity.Test
public async Task OnValidateIdentityRejectsWhenValidateSecurityStampFails() public async Task OnValidateIdentityRejectsWhenValidateSecurityStampFails()
{ {
var user = new TestUser("test"); 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 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), await RunApplicationCookieTest(user, httpContext, /*shouldStampValidate*/false, async () =>
new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1) }, {
IdentityConstants.ApplicationScheme); var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
Assert.NotNull(context.Properties); var ticket = new AuthenticationTicket(new ClaimsPrincipal(id),
Assert.NotNull(context.Options); new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1) },
Assert.NotNull(context.Principal); IdentityConstants.ApplicationScheme);
await SecurityStampValidator.ValidatePrincipalAsync(context);
Assert.Null(context.Principal); var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket);
signInManager.VerifyAll(); Assert.NotNull(context.Properties);
Assert.NotNull(context.Options);
Assert.NotNull(context.Principal);
await SecurityStampValidator.ValidatePrincipalAsync(context);
Assert.Null(context.Principal);
});
} }
[Fact] [Fact]
@ -216,5 +219,51 @@ namespace Microsoft.AspNetCore.Identity.Test
await SecurityStampValidator.ValidatePrincipalAsync(context); await SecurityStampValidator.ValidatePrincipalAsync(context);
Assert.NotNull(context.Principal); 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.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -17,7 +16,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.Test namespace Microsoft.AspNetCore.Identity.Test
{ {
public class SignManagerInTest public class SignInManagerTest
{ {
//[Theory] //[Theory]
//[InlineData(true)] //[InlineData(true)]

View File

@ -184,7 +184,8 @@ namespace Microsoft.AspNetCore.Identity.InMemory
[Fact] [Fact]
public async Task TwoFactorRememberCookieVerification() 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"); var transaction1 = await SendAsync(server, "http://example.com/createMe");
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode); 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); var transaction3 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
Assert.Equal(HttpStatusCode.OK, transaction3.Response.StatusCode); 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) 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"); var result = await userManager.CreateAsync(new TestUser("simple"), "aaaaaa");
res.StatusCode = result.Succeeded ? 200 : 500; 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)) 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); var result = await signInManager.IsTwoFactorClientRememberedAsync(user);
res.StatusCode = result ? 200 : 500; 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")) else if (req.Path == new PathString("/me"))
{ {
@ -295,7 +340,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory
}) })
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddIdentity<TestUser, TestRole>(); services.AddIdentity<TestUser, TestRole>().AddDefaultTokenProviders();
services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>(); services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>();
services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>(); services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>();
configureServices?.Invoke(services); configureServices?.Invoke(services);
@ -346,7 +391,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory
}; };
if (transaction.Response.Headers.Contains("Set-Cookie")) 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)) if (!string.IsNullOrEmpty(transaction.SetCookie))
{ {