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