mirror of https://github.com/abpframework/abp.git
21 changed files with 1599 additions and 34 deletions
@ -1,13 +1,36 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.AspNetCore; |
|||
using Volo.Abp.IdentityServer.AspNetIdentity; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.PermissionManagement.IdentityServer; |
|||
using Volo.Abp.SecurityLog; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace Volo.Abp.IdentityServer; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpIdentityAspNetCoreModule), |
|||
typeof(AbpIdentityServerTestEntityFrameworkCoreModule), |
|||
typeof(AbpPermissionManagementDomainIdentityServerModule) |
|||
)] |
|||
public class AbpIdentityServerDomainTestModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddSingleton<IdentityUserStoreFailureSimulator>(); |
|||
context.Services.AddSingleton<IdentityServerTestSettingValueProvider>(); |
|||
context.Services.Replace(ServiceDescriptor.Scoped<IdentityUserStore, TestIdentityUserStore>()); |
|||
context.Services.Replace(ServiceDescriptor.Singleton<IUnitOfWorkManager>( |
|||
serviceProvider => serviceProvider.GetRequiredService<UnitOfWorkManager>())); |
|||
|
|||
Configure<AbpSecurityLogOptions>(options => options.IsEnabled = false); |
|||
Configure<AbpSettingOptions>(options => |
|||
{ |
|||
options.ValueProviders.Add<IdentityServerTestSettingValueProvider>(); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,199 @@ |
|||
using System; |
|||
using System.Collections.Specialized; |
|||
using System.Threading.Tasks; |
|||
using IdentityServer4.Validation; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Shouldly; |
|||
using Volo.Abp.Identity.Settings; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.IdentityServer.AspNetIdentity; |
|||
|
|||
public class AbpResourceOwnerPasswordValidatorPasswordChange_Tests : AbpResourceOwnerPasswordValidatorTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Required_Password_Change_With_TwoFactor_Should_Require_Code_Before_Authenticating() |
|||
{ |
|||
await CreateUserAsync( |
|||
twoFactorEnabled: true, |
|||
shouldChangePasswordOnNextLogin: true, |
|||
addFailedAccess: true); |
|||
|
|||
var challengeContext = CreateContext(); |
|||
await ValidateAsync(challengeContext); |
|||
var changePasswordToken = AssertPasswordChangeChallenge( |
|||
challengeContext, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var passwordChangeContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
await ValidateAsync(passwordChangeContext); |
|||
|
|||
AssertRequiresTwoFactor(passwordChangeContext); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
var successContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}, NewPassword); |
|||
await ValidateAsync(successContext); |
|||
|
|||
successContext.Result.IsError.ShouldBeFalse(); |
|||
successContext.Result.Subject.ShouldNotBeNull(); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Required_Password_Change_Without_TwoFactor_Should_Authenticate_And_Reset_Failed_Count() |
|||
{ |
|||
await CreateUserAsync( |
|||
twoFactorEnabled: false, |
|||
shouldChangePasswordOnNextLogin: true, |
|||
addFailedAccess: true); |
|||
|
|||
var challengeContext = CreateContext(); |
|||
await ValidateAsync(challengeContext); |
|||
var changePasswordToken = AssertPasswordChangeChallenge( |
|||
challengeContext, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var successContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
await ValidateAsync(successContext); |
|||
|
|||
successContext.Result.IsError.ShouldBeFalse(); |
|||
successContext.Result.Subject.ShouldNotBeNull(); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
|
|||
var passwordState = await GetPasswordStateAsync(); |
|||
passwordState.OldPasswordIsValid.ShouldBeFalse(); |
|||
passwordState.NewPasswordIsValid.ShouldBeTrue(); |
|||
passwordState.ShouldChangePasswordOnNextLogin.ShouldBeFalse(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Periodic_Password_Change_With_TwoFactor_Should_Use_Periodic_Challenge_And_Require_Code() |
|||
{ |
|||
var settingValueProvider = GetRequiredService<IdentityServerTestSettingValueProvider>(); |
|||
settingValueProvider.Set( |
|||
IdentitySettingNames.Password.ForceUsersToPeriodicallyChangePassword, |
|||
true.ToString()); |
|||
settingValueProvider.Set( |
|||
IdentitySettingNames.Password.PasswordChangePeriodDays, |
|||
1.ToString()); |
|||
|
|||
try |
|||
{ |
|||
await CreateUserAsync( |
|||
twoFactorEnabled: true, |
|||
addFailedAccess: true, |
|||
lastPasswordChangeTime: DateTimeOffset.UtcNow.AddDays(-2)); |
|||
|
|||
var challengeContext = CreateContext(); |
|||
await ValidateAsync(challengeContext); |
|||
var changePasswordToken = AssertPasswordChangeChallenge( |
|||
challengeContext, |
|||
"PeriodicallyChangePassword"); |
|||
|
|||
var passwordChangeContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
await ValidateAsync(passwordChangeContext); |
|||
|
|||
AssertRequiresTwoFactor(passwordChangeContext); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
var successContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}, NewPassword); |
|||
await ValidateAsync(successContext); |
|||
|
|||
successContext.Result.IsError.ShouldBeFalse(); |
|||
successContext.Result.Subject.ShouldNotBeNull(); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
finally |
|||
{ |
|||
settingValueProvider.Clear(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Password_Change_Should_Roll_Back_When_Access_Failed_Count_Update_Fails() |
|||
{ |
|||
await CreateUserAsync( |
|||
twoFactorEnabled: true, |
|||
shouldChangePasswordOnNextLogin: true); |
|||
|
|||
var challengeContext = CreateContext(); |
|||
await ValidateAsync(challengeContext); |
|||
var changePasswordToken = AssertPasswordChangeChallenge( |
|||
challengeContext, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAfterSuccessfulUpdates(2); |
|||
var failedContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken, |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = "invalid-code" |
|||
}); |
|||
try |
|||
{ |
|||
await ValidateAsync(failedContext); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
failedContext.Result.IsError.ShouldBeTrue(); |
|||
failedContext.Result.Error.ShouldBe("invalid_grant"); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
|
|||
var passwordState = await GetPasswordStateAsync(); |
|||
passwordState.OldPasswordIsValid.ShouldBeTrue(); |
|||
passwordState.NewPasswordIsValid.ShouldBeFalse(); |
|||
passwordState.ShouldChangePasswordOnNextLogin.ShouldBeTrue(); |
|||
|
|||
var retryContext = CreateContext(); |
|||
await ValidateAsync(retryContext); |
|||
AssertPasswordChangeChallenge(retryContext, "ShouldChangePasswordOnNextLogin"); |
|||
} |
|||
|
|||
private static string AssertPasswordChangeChallenge( |
|||
ResourceOwnerPasswordValidationContext context, |
|||
string expectedErrorDescription) |
|||
{ |
|||
context.Result.IsError.ShouldBeTrue(); |
|||
context.Result.Error.ShouldBe("invalid_grant"); |
|||
context.Result.ErrorDescription.ShouldBe(expectedErrorDescription); |
|||
context.Result.CustomResponse.ShouldNotBeNull(); |
|||
context.Result.CustomResponse.ContainsKey("changePasswordToken").ShouldBeTrue(); |
|||
return context.Result.CustomResponse["changePasswordToken"].ToString(); |
|||
} |
|||
|
|||
private static void AssertRequiresTwoFactor(ResourceOwnerPasswordValidationContext context) |
|||
{ |
|||
context.Result.IsError.ShouldBeTrue(); |
|||
context.Result.Error.ShouldBe("invalid_grant"); |
|||
context.Result.ErrorDescription.ShouldBe("RequiresTwoFactor"); |
|||
context.Result.CustomResponse.ShouldNotBeNull(); |
|||
context.Result.CustomResponse.ContainsKey("twoFactorToken").ShouldBeTrue(); |
|||
} |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
using System; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using IdentityServer4.Models; |
|||
using IdentityServer4.Validation; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Uow; |
|||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|||
|
|||
namespace Volo.Abp.IdentityServer.AspNetIdentity; |
|||
|
|||
public abstract class AbpResourceOwnerPasswordValidatorTestBase : AbpIdentityServerDomainTestBase |
|||
{ |
|||
protected const string UserName = "password-grant-user"; |
|||
protected const string Password = "1q2w3E*"; |
|||
protected const string NewPassword = "2q3w4E*"; |
|||
|
|||
protected Task CreateUserAsync( |
|||
bool twoFactorEnabled, |
|||
bool shouldChangePasswordOnNextLogin = false, |
|||
bool addFailedAccess = false, |
|||
DateTimeOffset? lastPasswordChangeTime = null) |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = new IdentityUser(Guid.NewGuid(), UserName, UserName + "@abp.io"); |
|||
user.SetEmailConfirmed(true); |
|||
user.SetShouldChangePasswordOnNextLogin(shouldChangePasswordOnNextLogin); |
|||
|
|||
(await userManager.CreateAsync(user, Password)).CheckErrors(); |
|||
(await userManager.SetLockoutEnabledAsync(user, true)).CheckErrors(); |
|||
(await userManager.SetTwoFactorEnabledAsync(user, twoFactorEnabled)).CheckErrors(); |
|||
|
|||
if (lastPasswordChangeTime.HasValue) |
|||
{ |
|||
user.SetLastPasswordChangeTime(lastPasswordChangeTime); |
|||
(await userManager.UpdateAsync(user)).CheckErrors(); |
|||
} |
|||
|
|||
if (addFailedAccess) |
|||
{ |
|||
(await userManager.AccessFailedAsync(user)).CheckErrors(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected Task<string> GenerateTwoFactorCodeAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(UserName); |
|||
return await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); |
|||
}); |
|||
} |
|||
|
|||
protected Task<string> GenerateRecoveryCodeAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(UserName); |
|||
var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 1); |
|||
return recoveryCodes.Single(); |
|||
}); |
|||
} |
|||
|
|||
protected Task<int> GetAccessFailedCountAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(UserName); |
|||
return await userManager.GetAccessFailedCountAsync(user); |
|||
}); |
|||
} |
|||
|
|||
protected Task<(bool OldPasswordIsValid, bool NewPasswordIsValid, bool ShouldChangePasswordOnNextLogin)> GetPasswordStateAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(UserName); |
|||
return ( |
|||
OldPasswordIsValid: await userManager.CheckPasswordAsync(user, Password), |
|||
NewPasswordIsValid: await userManager.CheckPasswordAsync(user, NewPassword), |
|||
user.ShouldChangePasswordOnNextLogin); |
|||
}); |
|||
} |
|||
|
|||
protected Task ValidateAsync(ResourceOwnerPasswordValidationContext context) |
|||
{ |
|||
return WithUnitOfWorkAsync( |
|||
new AbpUnitOfWorkOptions { IsTransactional = true }, |
|||
async serviceProvider => |
|||
{ |
|||
var unitOfWorkManager = serviceProvider.GetRequiredService<IUnitOfWorkManager>(); |
|||
unitOfWorkManager.Current.ShouldNotBeNull(); |
|||
unitOfWorkManager.Current.Options.IsTransactional.ShouldBeTrue(); |
|||
|
|||
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); |
|||
var originalHttpContext = httpContextAccessor.HttpContext; |
|||
httpContextAccessor.HttpContext = new DefaultHttpContext |
|||
{ |
|||
RequestServices = serviceProvider |
|||
}; |
|||
try |
|||
{ |
|||
await serviceProvider |
|||
.GetRequiredService<IResourceOwnerPasswordValidator>() |
|||
.ValidateAsync(context); |
|||
} |
|||
finally |
|||
{ |
|||
httpContextAccessor.HttpContext = originalHttpContext; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected static ResourceOwnerPasswordValidationContext CreateContext( |
|||
NameValueCollection raw = null, |
|||
string password = null) |
|||
{ |
|||
return new ResourceOwnerPasswordValidationContext |
|||
{ |
|||
UserName = UserName, |
|||
Password = password ?? Password, |
|||
Request = new ValidatedTokenRequest |
|||
{ |
|||
Raw = raw ?? new NameValueCollection(), |
|||
Client = new Client { ClientId = "test-client" } |
|||
} |
|||
}; |
|||
} |
|||
|
|||
protected Task WithUnitOfWorkAsync(Func<IServiceProvider, Task> action) |
|||
{ |
|||
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), action); |
|||
} |
|||
|
|||
protected async Task WithUnitOfWorkAsync( |
|||
AbpUnitOfWorkOptions options, |
|||
Func<IServiceProvider, Task> action) |
|||
{ |
|||
using var scope = ServiceProvider.CreateScope(); |
|||
var unitOfWorkManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); |
|||
using var uow = unitOfWorkManager.Begin(options); |
|||
await action(scope.ServiceProvider); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
protected Task<TResult> WithUnitOfWorkAsync<TResult>(Func<IServiceProvider, Task<TResult>> action) |
|||
{ |
|||
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), action); |
|||
} |
|||
|
|||
protected async Task<TResult> WithUnitOfWorkAsync<TResult>( |
|||
AbpUnitOfWorkOptions options, |
|||
Func<IServiceProvider, Task<TResult>> action) |
|||
{ |
|||
using var scope = ServiceProvider.CreateScope(); |
|||
var unitOfWorkManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); |
|||
using var uow = unitOfWorkManager.Begin(options); |
|||
var result = await action(scope.ServiceProvider); |
|||
await uow.CompleteAsync(); |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,187 @@ |
|||
using System.Collections.Specialized; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.IdentityServer.AspNetIdentity; |
|||
|
|||
public class AbpResourceOwnerPasswordValidator_Tests : AbpResourceOwnerPasswordValidatorTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Invalid_Recovery_Code_Should_Not_Be_Overwritten_By_RequiresTwoFactor() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: true); |
|||
var context = CreateContext(new NameValueCollection |
|||
{ |
|||
["RecoveryCode"] = "invalid-recovery-code" |
|||
}); |
|||
|
|||
await ValidateAsync(context); |
|||
|
|||
context.Result.IsError.ShouldBeTrue(); |
|||
context.Result.Error.ShouldBe("invalid_grant"); |
|||
context.Result.ErrorDescription.ShouldBe("Invalid recovery code!"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Recovery_Code_Should_Remain_Usable_When_Redemption_Update_Fails() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: true); |
|||
var recoveryCode = await GenerateRecoveryCodeAsync(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAfterSuccessfulUpdates(0); |
|||
var failedContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
try |
|||
{ |
|||
await ValidateAsync(failedContext); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
failedContext.Result.IsError.ShouldBeTrue(); |
|||
failedContext.Result.Error.ShouldBe("invalid_grant"); |
|||
|
|||
var retryContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
await ValidateAsync(retryContext); |
|||
|
|||
retryContext.Result.IsError.ShouldBeFalse(); |
|||
retryContext.Result.Subject.ShouldNotBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ChangePassword_Update_Exception_Should_Return_InvalidGrant_And_Preserve_User_State() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: false, shouldChangePasswordOnNextLogin: true); |
|||
var challengeContext = CreateContext(); |
|||
await ValidateAsync(challengeContext); |
|||
var changePasswordToken = challengeContext.Result.CustomResponse["changePasswordToken"].ToString(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAfterSuccessfulUpdates(0); |
|||
var context = CreateContext(new NameValueCollection |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
try |
|||
{ |
|||
await ValidateAsync(context); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
context.Result.IsError.ShouldBeTrue(); |
|||
context.Result.Error.ShouldBe("invalid_grant"); |
|||
|
|||
var userState = await GetPasswordStateAsync(); |
|||
userState.OldPasswordIsValid.ShouldBeTrue(); |
|||
userState.NewPasswordIsValid.ShouldBeFalse(); |
|||
userState.ShouldChangePasswordOnNextLogin.ShouldBeTrue(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Valid_TwoFactor_Code_Should_Reset_Access_Failed_Count() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: true, addFailedAccess: true); |
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
var context = CreateContext(new NameValueCollection |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}); |
|||
|
|||
await ValidateAsync(context); |
|||
|
|||
context.Result.IsError.ShouldBeFalse(); |
|||
context.Result.Subject.ShouldNotBeNull(); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Valid_Recovery_Code_Should_Reset_Access_Failed_Count() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: true, addFailedAccess: true); |
|||
var recoveryCode = await GenerateRecoveryCodeAsync(); |
|||
var context = CreateContext(new NameValueCollection |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
|
|||
await ValidateAsync(context); |
|||
|
|||
context.Result.IsError.ShouldBeFalse(); |
|||
context.Result.Subject.ShouldNotBeNull(); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Reset_Failure_Should_Not_Authenticate() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: true, addFailedAccess: true); |
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAccessFailedCountReset(); |
|||
var context = CreateContext(new NameValueCollection |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}); |
|||
try |
|||
{ |
|||
await ValidateAsync(context); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
context.Result.IsError.ShouldBeTrue(); |
|||
context.Result.Error.ShouldBe("invalid_grant"); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Recovery_Code_Should_Remain_Usable_When_Reset_Failure_Rolls_Back_Request() |
|||
{ |
|||
await CreateUserAsync(twoFactorEnabled: true, addFailedAccess: true); |
|||
var recoveryCode = await GenerateRecoveryCodeAsync(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAccessFailedCountReset(); |
|||
var failedContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
try |
|||
{ |
|||
await ValidateAsync(failedContext); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
failedContext.Result.IsError.ShouldBeTrue(); |
|||
failedContext.Result.Error.ShouldBe("invalid_grant"); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var retryContext = CreateContext(new NameValueCollection |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
await ValidateAsync(retryContext); |
|||
|
|||
retryContext.Result.IsError.ShouldBeFalse(); |
|||
retryContext.Result.Subject.ShouldNotBeNull(); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace Volo.Abp.IdentityServer.AspNetIdentity; |
|||
|
|||
public class IdentityServerTestSettingValueProvider : ISettingValueProvider |
|||
{ |
|||
public const string ProviderName = "IdentityServerDomainTest"; |
|||
|
|||
private readonly Dictionary<string, string> _values = new(); |
|||
|
|||
public string Name => ProviderName; |
|||
|
|||
public void Set(string name, string value) |
|||
{ |
|||
_values[name] = value; |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
_values.Clear(); |
|||
} |
|||
|
|||
public Task<string> GetOrNullAsync(SettingDefinition setting) |
|||
{ |
|||
return Task.FromResult(_values.GetOrDefault(setting.Name)); |
|||
} |
|||
|
|||
public Task<List<SettingValue>> GetAllAsync(SettingDefinition[] settings) |
|||
{ |
|||
return Task.FromResult(settings |
|||
.Select(setting => new SettingValue(setting.Name, _values.GetOrDefault(setting.Name))) |
|||
.ToList()); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
namespace Volo.Abp.IdentityServer.AspNetIdentity; |
|||
|
|||
public class IdentityUserStoreFailureSimulator |
|||
{ |
|||
private int? _successfulUpdatesBeforeFailure; |
|||
|
|||
public bool IsAccessFailedCountResetFailureEnabled { get; private set; } |
|||
|
|||
public void FailAccessFailedCountReset() |
|||
{ |
|||
IsAccessFailedCountResetFailureEnabled = true; |
|||
} |
|||
|
|||
public void FailAfterSuccessfulUpdates(int successfulUpdateCount) |
|||
{ |
|||
_successfulUpdatesBeforeFailure = successfulUpdateCount; |
|||
} |
|||
|
|||
public bool ShouldFailUpdate() |
|||
{ |
|||
if (!_successfulUpdatesBeforeFailure.HasValue) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (_successfulUpdatesBeforeFailure.Value > 0) |
|||
{ |
|||
_successfulUpdatesBeforeFailure--; |
|||
return false; |
|||
} |
|||
|
|||
_successfulUpdatesBeforeFailure = null; |
|||
return true; |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
IsAccessFailedCountResetFailureEnabled = false; |
|||
_successfulUpdatesBeforeFailure = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp.Guids; |
|||
using Volo.Abp.Identity; |
|||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|||
|
|||
namespace Volo.Abp.IdentityServer.AspNetIdentity; |
|||
|
|||
public class TestIdentityUserStore : IdentityUserStore |
|||
{ |
|||
private readonly IdentityUserStoreFailureSimulator _failureSimulator; |
|||
private bool _failNextUpdate; |
|||
|
|||
public TestIdentityUserStore( |
|||
IIdentityUserRepository userRepository, |
|||
IIdentityRoleRepository roleRepository, |
|||
IGuidGenerator guidGenerator, |
|||
ILogger<IdentityRoleStore> logger, |
|||
ILookupNormalizer lookupNormalizer, |
|||
IdentityErrorDescriber describer, |
|||
IdentityUserStoreFailureSimulator failureSimulator) |
|||
: base(userRepository, roleRepository, guidGenerator, logger, lookupNormalizer, describer) |
|||
{ |
|||
_failureSimulator = failureSimulator; |
|||
} |
|||
|
|||
public override Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken = default) |
|||
{ |
|||
_failNextUpdate = _failureSimulator.IsAccessFailedCountResetFailureEnabled; |
|||
return base.ResetAccessFailedCountAsync(user, cancellationToken); |
|||
} |
|||
|
|||
public override Task<IdentityResult> UpdateAsync(IdentityUser user, CancellationToken cancellationToken = default) |
|||
{ |
|||
if (_failNextUpdate || _failureSimulator.ShouldFailUpdate()) |
|||
{ |
|||
_failNextUpdate = false; |
|||
return Task.FromResult(IdentityResult.Failed(new IdentityError |
|||
{ |
|||
Code = "IdentityUserUpdateFailed", |
|||
Description = "The identity user could not be updated." |
|||
})); |
|||
} |
|||
|
|||
return base.UpdateAsync(user, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class IdentityUserStoreFailureSimulator |
|||
{ |
|||
private int? _successfulUpdatesBeforeFailure; |
|||
|
|||
public bool IsAccessFailedCountResetFailureEnabled { get; private set; } |
|||
|
|||
public void FailAccessFailedCountReset() |
|||
{ |
|||
IsAccessFailedCountResetFailureEnabled = true; |
|||
} |
|||
|
|||
public void FailAfterSuccessfulUpdates(int successfulUpdateCount) |
|||
{ |
|||
_successfulUpdatesBeforeFailure = successfulUpdateCount; |
|||
} |
|||
|
|||
public bool ShouldFailUpdate() |
|||
{ |
|||
if (!_successfulUpdatesBeforeFailure.HasValue) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (_successfulUpdatesBeforeFailure.Value > 0) |
|||
{ |
|||
_successfulUpdatesBeforeFailure--; |
|||
return false; |
|||
} |
|||
|
|||
_successfulUpdatesBeforeFailure = null; |
|||
return true; |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
IsAccessFailedCountResetFailureEnabled = false; |
|||
_successfulUpdatesBeforeFailure = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using System.Text.Json; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.AspNetCore.TestBase; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public abstract class OpenIddictPasswordGrantIntegrationTestBase : AbpWebApplicationFactoryIntegratedTest<Program> |
|||
{ |
|||
protected const string NewPassword = "2q3w4E*"; |
|||
|
|||
protected Task<HttpResponseMessage> RequestPasswordTokenAsync( |
|||
Dictionary<string, string> additionalParameters = null, |
|||
string password = null) |
|||
{ |
|||
var parameters = new Dictionary<string, string> |
|||
{ |
|||
["grant_type"] = "password", |
|||
["client_id"] = "test-client", |
|||
["client_secret"] = "test-secret", |
|||
["username"] = OpenIddictPasswordGrantTestData.UserName, |
|||
["password"] = password ?? OpenIddictPasswordGrantTestData.Password |
|||
}; |
|||
|
|||
if (additionalParameters != null) |
|||
{ |
|||
foreach (var parameter in additionalParameters) |
|||
{ |
|||
parameters[parameter.Key] = parameter.Value; |
|||
} |
|||
} |
|||
|
|||
return Client.PostAsync("/connect/token", new FormUrlEncodedContent(parameters)); |
|||
} |
|||
|
|||
protected static async Task AssertAccessTokenAsync(HttpResponseMessage response) |
|||
{ |
|||
response.StatusCode.ShouldBe(HttpStatusCode.OK); |
|||
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
|||
document.RootElement.GetProperty("access_token").GetString().ShouldNotBeNullOrWhiteSpace(); |
|||
} |
|||
|
|||
protected static async Task AssertInvalidGrantAsync(HttpResponseMessage response) |
|||
{ |
|||
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); |
|||
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
|||
var root = document.RootElement; |
|||
root.GetProperty("error").GetString().ShouldBe("invalid_grant"); |
|||
root.TryGetProperty("access_token", out _).ShouldBeFalse(); |
|||
} |
|||
|
|||
protected Task<string> GenerateTwoFactorCodeAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
return await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); |
|||
}); |
|||
} |
|||
|
|||
protected Task<int> GetAccessFailedCountAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync( |
|||
new AbpUnitOfWorkOptions { IsTransactional = false }, |
|||
async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
return await userManager.GetAccessFailedCountAsync(user); |
|||
}); |
|||
} |
|||
|
|||
protected virtual Task WithUnitOfWorkAsync(Func<IServiceProvider, Task> action) |
|||
{ |
|||
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), action); |
|||
} |
|||
|
|||
protected virtual async Task WithUnitOfWorkAsync( |
|||
AbpUnitOfWorkOptions options, |
|||
Func<IServiceProvider, Task> action) |
|||
{ |
|||
using var scope = ServiceProvider.CreateScope(); |
|||
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); |
|||
using var uow = uowManager.Begin(options); |
|||
await action(scope.ServiceProvider); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
protected virtual Task<TResult> WithUnitOfWorkAsync<TResult>(Func<IServiceProvider, Task<TResult>> action) |
|||
{ |
|||
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), action); |
|||
} |
|||
|
|||
protected virtual async Task<TResult> WithUnitOfWorkAsync<TResult>( |
|||
AbpUnitOfWorkOptions options, |
|||
Func<IServiceProvider, Task<TResult>> action) |
|||
{ |
|||
using var scope = ServiceProvider.CreateScope(); |
|||
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); |
|||
using var uow = uowManager.Begin(options); |
|||
var result = await action(scope.ServiceProvider); |
|||
await uow.CompleteAsync(); |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,270 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using System.Text.Json; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.Settings; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class OpenIddictPasswordGrantPasswordChange_Integration_Tests : OpenIddictPasswordGrantIntegrationTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Required_Password_Change_With_TwoFactor_Should_Require_Code_Before_Issuing_Token() |
|||
{ |
|||
await ConfigurePasswordChangeUserAsync( |
|||
twoFactorEnabled: true, |
|||
shouldChangePasswordOnNextLogin: true, |
|||
addFailedAccess: true); |
|||
|
|||
var challenge = await RequestPasswordTokenAsync(); |
|||
var changePasswordToken = await AssertPasswordChangeChallengeAsync( |
|||
challenge, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var passwordChangeResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
|
|||
await AssertRequiresTwoFactorAsync(passwordChangeResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
var successResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}, NewPassword); |
|||
|
|||
await AssertAccessTokenAsync(successResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Required_Password_Change_Without_TwoFactor_Should_Issue_Token_And_Reset_Failed_Count() |
|||
{ |
|||
await ConfigurePasswordChangeUserAsync( |
|||
twoFactorEnabled: false, |
|||
shouldChangePasswordOnNextLogin: true, |
|||
addFailedAccess: true); |
|||
|
|||
var challenge = await RequestPasswordTokenAsync(); |
|||
var changePasswordToken = await AssertPasswordChangeChallengeAsync( |
|||
challenge, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var response = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
|
|||
await AssertAccessTokenAsync(response); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Periodic_Password_Change_With_TwoFactor_Should_Require_Code_Before_Issuing_Token() |
|||
{ |
|||
var settingValueProvider = GetRequiredService<OpenIddictTestSettingValueProvider>(); |
|||
settingValueProvider.Set( |
|||
IdentitySettingNames.Password.ForceUsersToPeriodicallyChangePassword, |
|||
true.ToString()); |
|||
settingValueProvider.Set( |
|||
IdentitySettingNames.Password.PasswordChangePeriodDays, |
|||
1.ToString()); |
|||
|
|||
try |
|||
{ |
|||
await ConfigurePasswordChangeUserAsync( |
|||
twoFactorEnabled: true, |
|||
addFailedAccess: true, |
|||
lastPasswordChangeTime: DateTimeOffset.UtcNow.AddDays(-2)); |
|||
|
|||
var challenge = await RequestPasswordTokenAsync(); |
|||
var changePasswordToken = await AssertPasswordChangeChallengeAsync( |
|||
challenge, |
|||
"PeriodicallyChangePassword"); |
|||
|
|||
var response = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
|
|||
await AssertRequiresTwoFactorAsync(response); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
var successResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}, NewPassword); |
|||
|
|||
await AssertAccessTokenAsync(successResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
finally |
|||
{ |
|||
settingValueProvider.Clear(); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0)] |
|||
[InlineData(1)] |
|||
public async Task Password_Change_Update_Failure_Should_Roll_Back_Password_And_Required_Change_State( |
|||
int successfulUpdatesBeforeFailure) |
|||
{ |
|||
await ConfigurePasswordChangeUserAsync( |
|||
twoFactorEnabled: false, |
|||
shouldChangePasswordOnNextLogin: true); |
|||
|
|||
var challenge = await RequestPasswordTokenAsync(); |
|||
var changePasswordToken = await AssertPasswordChangeChallengeAsync( |
|||
challenge, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAfterSuccessfulUpdates(successfulUpdatesBeforeFailure); |
|||
HttpResponseMessage failedResponse; |
|||
try |
|||
{ |
|||
failedResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken |
|||
}); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
await AssertInvalidGrantAsync(failedResponse); |
|||
|
|||
var passwordState = await GetPasswordStateAsync(); |
|||
passwordState.OldPasswordIsValid.ShouldBeTrue(); |
|||
passwordState.NewPasswordIsValid.ShouldBeFalse(); |
|||
passwordState.ShouldChangePasswordOnNextLogin.ShouldBeTrue(); |
|||
|
|||
var retryChallenge = await RequestPasswordTokenAsync(); |
|||
await AssertPasswordChangeChallengeAsync( |
|||
retryChallenge, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Password_Change_Should_Roll_Back_When_Access_Failed_Count_Update_Fails() |
|||
{ |
|||
await ConfigurePasswordChangeUserAsync( |
|||
twoFactorEnabled: true, |
|||
shouldChangePasswordOnNextLogin: true); |
|||
|
|||
var challenge = await RequestPasswordTokenAsync(); |
|||
var changePasswordToken = await AssertPasswordChangeChallengeAsync( |
|||
challenge, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
|
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAfterSuccessfulUpdates(2); |
|||
HttpResponseMessage failedResponse; |
|||
try |
|||
{ |
|||
failedResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["NewPassword"] = NewPassword, |
|||
["ChangePasswordToken"] = changePasswordToken, |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = "invalid-code" |
|||
}); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
await AssertInvalidGrantAsync(failedResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
|
|||
var passwordState = await GetPasswordStateAsync(); |
|||
passwordState.OldPasswordIsValid.ShouldBeTrue(); |
|||
passwordState.NewPasswordIsValid.ShouldBeFalse(); |
|||
passwordState.ShouldChangePasswordOnNextLogin.ShouldBeTrue(); |
|||
|
|||
var retryChallenge = await RequestPasswordTokenAsync(); |
|||
await AssertPasswordChangeChallengeAsync( |
|||
retryChallenge, |
|||
"ShouldChangePasswordOnNextLogin"); |
|||
} |
|||
|
|||
private static async Task<string> AssertPasswordChangeChallengeAsync( |
|||
HttpResponseMessage response, |
|||
string expectedErrorDescription) |
|||
{ |
|||
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); |
|||
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
|||
var root = document.RootElement; |
|||
root.GetProperty("error_description").GetString().ShouldBe(expectedErrorDescription); |
|||
root.TryGetProperty("access_token", out _).ShouldBeFalse(); |
|||
return root.GetProperty("changePasswordToken").GetString(); |
|||
} |
|||
|
|||
private static async Task AssertRequiresTwoFactorAsync(HttpResponseMessage response) |
|||
{ |
|||
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); |
|||
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
|||
var root = document.RootElement; |
|||
root.GetProperty("error_description").GetString().ShouldBe("RequiresTwoFactor"); |
|||
root.TryGetProperty("access_token", out _).ShouldBeFalse(); |
|||
} |
|||
|
|||
private Task ConfigurePasswordChangeUserAsync( |
|||
bool twoFactorEnabled, |
|||
bool shouldChangePasswordOnNextLogin = false, |
|||
bool addFailedAccess = false, |
|||
DateTimeOffset? lastPasswordChangeTime = null) |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
user.SetShouldChangePasswordOnNextLogin(shouldChangePasswordOnNextLogin); |
|||
if (lastPasswordChangeTime.HasValue) |
|||
{ |
|||
user.SetLastPasswordChangeTime(lastPasswordChangeTime); |
|||
} |
|||
|
|||
(await userManager.SetTwoFactorEnabledAsync(user, twoFactorEnabled)).CheckErrors(); |
|||
if (addFailedAccess) |
|||
{ |
|||
(await userManager.AccessFailedAsync(user)).CheckErrors(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private Task<(bool OldPasswordIsValid, bool NewPasswordIsValid, bool ShouldChangePasswordOnNextLogin)> GetPasswordStateAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync( |
|||
new AbpUnitOfWorkOptions { IsTransactional = false }, |
|||
async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
return ( |
|||
OldPasswordIsValid: await userManager.CheckPasswordAsync(user, OpenIddictPasswordGrantTestData.Password), |
|||
NewPasswordIsValid: await userManager.CheckPasswordAsync(user, NewPassword), |
|||
user.ShouldChangePasswordOnNextLogin); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public static class OpenIddictPasswordGrantTestData |
|||
{ |
|||
public const string UserName = "two-factor-user"; |
|||
public const string Email = "two-factor-user@abp.io"; |
|||
public const string Password = "1q2w3E*"; |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.Identity; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class OpenIddictPasswordGrantTwoFactor_Integration_Tests : OpenIddictPasswordGrantIntegrationTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Password_Stage_Should_Not_Reset_Access_Failed_Count() |
|||
{ |
|||
var invalidCodeResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = "invalid-code" |
|||
}); |
|||
|
|||
invalidCodeResponse.StatusCode.ShouldBe(HttpStatusCode.BadRequest); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var passwordStageResponse = await RequestPasswordTokenAsync(); |
|||
|
|||
passwordStageResponse.StatusCode.ShouldBe(HttpStatusCode.BadRequest); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Valid_TwoFactor_Code_Should_Reset_Access_Failed_Count_Before_Issuing_Token() |
|||
{ |
|||
var code = await GenerateTwoFactorCodeAsync(); |
|||
await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = "invalid-code" |
|||
}); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var response = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}); |
|||
|
|||
await AssertAccessTokenAsync(response); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Valid_Recovery_Code_Should_Reset_Access_Failed_Count_Before_Issuing_Token() |
|||
{ |
|||
var recoveryCode = await GenerateRecoveryCodeAfterFailedAccessAsync(); |
|||
|
|||
var response = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
|
|||
await AssertAccessTokenAsync(response); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Reset_Failure_Should_Not_Issue_Token() |
|||
{ |
|||
var code = await GenerateTwoFactorCodeAfterFailedAccessAsync(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAccessFailedCountReset(); |
|||
HttpResponseMessage response; |
|||
try |
|||
{ |
|||
response = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["TwoFactorProvider"] = TokenOptions.DefaultEmailProvider, |
|||
["TwoFactorCode"] = code |
|||
}); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
await AssertInvalidGrantAsync(response); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Recovery_Code_Should_Remain_Usable_When_Reset_Failure_Rolls_Back_Request() |
|||
{ |
|||
var recoveryCode = await GenerateRecoveryCodeAfterFailedAccessAsync(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAccessFailedCountReset(); |
|||
HttpResponseMessage failedResponse; |
|||
try |
|||
{ |
|||
failedResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
await AssertInvalidGrantAsync(failedResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var retryResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
|
|||
await AssertAccessTokenAsync(retryResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Recovery_Code_Should_Remain_Usable_When_Redemption_Update_Fails() |
|||
{ |
|||
var recoveryCode = await GenerateRecoveryCodeAfterFailedAccessAsync(); |
|||
var failureSimulator = GetRequiredService<IdentityUserStoreFailureSimulator>(); |
|||
failureSimulator.FailAfterSuccessfulUpdates(0); |
|||
HttpResponseMessage failedResponse; |
|||
try |
|||
{ |
|||
failedResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
} |
|||
finally |
|||
{ |
|||
failureSimulator.Reset(); |
|||
} |
|||
|
|||
await AssertInvalidGrantAsync(failedResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(1); |
|||
|
|||
var retryResponse = await RequestPasswordTokenAsync(new Dictionary<string, string> |
|||
{ |
|||
["RecoveryCode"] = recoveryCode |
|||
}); |
|||
|
|||
await AssertAccessTokenAsync(retryResponse); |
|||
(await GetAccessFailedCountAsync()).ShouldBe(0); |
|||
} |
|||
|
|||
private Task<string> GenerateTwoFactorCodeAfterFailedAccessAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
(await userManager.AccessFailedAsync(user)).CheckErrors(); |
|||
return await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); |
|||
}); |
|||
} |
|||
|
|||
private Task<string> GenerateRecoveryCodeAfterFailedAccessAsync() |
|||
{ |
|||
return WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
(await userManager.AccessFailedAsync(user)).CheckErrors(); |
|||
var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 1); |
|||
return recoveryCodes.Single(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class OpenIddictPasswordGrant_Integration_Tests : OpenIddictPasswordGrantIntegrationTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Password_Grant_Without_TwoFactor_Should_Issue_Token() |
|||
{ |
|||
await WithUnitOfWorkAsync(async serviceProvider => |
|||
{ |
|||
var userManager = serviceProvider.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName); |
|||
(await userManager.SetTwoFactorEnabledAsync(user, false)).CheckErrors(); |
|||
}); |
|||
|
|||
var response = await RequestPasswordTokenAsync(); |
|||
|
|||
await AssertAccessTokenAsync(response); |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class OpenIddictTestSettingValueProvider : ISettingValueProvider |
|||
{ |
|||
public const string ProviderName = "OpenIddictIntegrationTest"; |
|||
|
|||
private readonly Dictionary<string, string> _values = new(); |
|||
|
|||
public string Name => ProviderName; |
|||
|
|||
public void Set(string name, string value) |
|||
{ |
|||
_values[name] = value; |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
_values.Clear(); |
|||
} |
|||
|
|||
public Task<string> GetOrNullAsync(SettingDefinition setting) |
|||
{ |
|||
return Task.FromResult(_values.GetOrDefault(setting.Name)); |
|||
} |
|||
|
|||
public Task<List<SettingValue>> GetAllAsync(SettingDefinition[] settings) |
|||
{ |
|||
return Task.FromResult(settings |
|||
.Select(setting => new SettingValue(setting.Name, _values.GetOrDefault(setting.Name))) |
|||
.ToList()); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp.Guids; |
|||
using Volo.Abp.Identity; |
|||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|||
|
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class TestIdentityUserStore : IdentityUserStore |
|||
{ |
|||
private readonly IdentityUserStoreFailureSimulator _failureSimulator; |
|||
private bool _failNextUpdate; |
|||
|
|||
public TestIdentityUserStore( |
|||
IIdentityUserRepository userRepository, |
|||
IIdentityRoleRepository roleRepository, |
|||
IGuidGenerator guidGenerator, |
|||
ILogger<IdentityRoleStore> logger, |
|||
ILookupNormalizer lookupNormalizer, |
|||
IdentityErrorDescriber describer, |
|||
IdentityUserStoreFailureSimulator failureSimulator) |
|||
: base(userRepository, roleRepository, guidGenerator, logger, lookupNormalizer, describer) |
|||
{ |
|||
_failureSimulator = failureSimulator; |
|||
} |
|||
|
|||
public override Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken = default) |
|||
{ |
|||
_failNextUpdate = _failureSimulator.IsAccessFailedCountResetFailureEnabled; |
|||
return base.ResetAccessFailedCountAsync(user, cancellationToken); |
|||
} |
|||
|
|||
public override Task<IdentityResult> UpdateAsync(IdentityUser user, CancellationToken cancellationToken = default) |
|||
{ |
|||
if (_failNextUpdate || _failureSimulator.ShouldFailUpdate()) |
|||
{ |
|||
_failNextUpdate = false; |
|||
return Task.FromResult(IdentityResult.Failed(new IdentityError |
|||
{ |
|||
Code = "IdentityUserUpdateFailed", |
|||
Description = "The identity user could not be updated." |
|||
})); |
|||
} |
|||
|
|||
return base.UpdateAsync(user, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.OpenIddict.Integration; |
|||
|
|||
public class TokenVisibilityRecorder |
|||
{ |
|||
public long? TokenCountAtResponseStart { get; set; } |
|||
} |
|||
Loading…
Reference in new issue