Browse Source

Fix password grant two-factor authentication flow

pull/26063/head
maliming 2 days ago
parent
commit
e567df5598
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 69
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs
  2. 1
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo.Abp.IdentityServer.Domain.Tests.csproj
  3. 25
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AbpIdentityServerDomainTestModule.cs
  4. 199
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidatorPasswordChange_Tests.cs
  5. 174
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidatorTestBase.cs
  6. 187
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator_Tests.cs
  7. 37
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/IdentityServerTestSettingValueProvider.cs
  8. 41
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/IdentityUserStoreFailureSimulator.cs
  9. 49
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/TestIdentityUserStore.cs
  10. 67
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs
  11. 2
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo.Abp.OpenIddict.AspNetCore.Tests.csproj
  12. 41
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/IdentityUserStoreFailureSimulator.cs
  13. 114
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantIntegrationTestBase.cs
  14. 270
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantPasswordChange_Integration_Tests.cs
  15. 8
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantTestData.cs
  16. 177
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantTwoFactor_Integration_Tests.cs
  17. 25
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrant_Integration_Tests.cs
  18. 37
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTestSettingValueProvider.cs
  19. 55
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs
  20. 49
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/TestIdentityUserStore.cs
  21. 6
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/TokenVisibilityRecorder.cs

69
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs

@ -41,6 +41,8 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
protected ISettingProvider SettingProvider { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public AbpResourceOwnerPasswordValidator(
IdentityUserManager userManager,
SignInManager<IdentityUser> signInManager,
@ -50,7 +52,8 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
IOptions<AbpIdentityOptions> abpIdentityOptions,
IServiceScopeFactory serviceScopeFactory,
IOptions<IdentityOptions> identityOptions,
ISettingProvider settingProvider)
ISettingProvider settingProvider,
IUnitOfWorkManager unitOfWorkManager)
{
UserManager = userManager;
SignInManager = signInManager;
@ -61,6 +64,7 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
AbpIdentityOptions = abpIdentityOptions.Value;
IdentityOptions = identityOptions;
SettingProvider = settingProvider;
UnitOfWorkManager = unitOfWorkManager;
}
/// <summary>
@ -197,7 +201,8 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
}
Logger.LogInformation("Authentication failed for username: {username}, reason: InvalidRecoveryCode", context.UserName);
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, Localizer["InvalidRecoveryCode"]);
await RollbackAndSetInvalidGrantResultAsync(context, Localizer["InvalidRecoveryCode"]);
return;
}
var twoFactorProvider = context.Request?.Raw?["TwoFactorProvider"];
@ -211,7 +216,12 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
return;
}
await UserManager.AccessFailedAsync(user);
var accessFailedResult = await UserManager.AccessFailedAsync(user);
if (!accessFailedResult.Succeeded)
{
await RollbackAndSetInvalidGrantResultAsync(context, Localizer["InvalidUserNameOrPassword"]);
return;
}
Logger.LogInformation("Authentication failed for username: {username}, reason: InvalidAuthenticatorCode", context.UserName);
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, Localizer["InvalidAuthenticatorCode"]);
@ -255,7 +265,16 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordType.ToString(), changePasswordToken))
{
var changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword);
IdentityResult changePasswordResult;
try
{
changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword);
}
catch (AbpIdentityResultException exception)
{
changePasswordResult = exception.IdentityResult;
}
if (changePasswordResult.Succeeded)
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
@ -271,13 +290,29 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
user.SetShouldChangePasswordOnNextLogin(false);
}
await UserManager.UpdateAsync(user);
await SetSuccessResultAsync(context, user);
var updateUserResult = await UserManager.UpdateAsync(user);
if (!updateUserResult.Succeeded)
{
await RollbackAndSetInvalidGrantResultAsync(context, Localizer["InvalidUserNameOrPassword"]);
return;
}
if (await IsTfaEnabledAsync(user))
{
await HandleTwoFactorLoginAsync(context, user);
}
else
{
await SetSuccessResultAsync(context, user);
}
}
else
{
Logger.LogInformation("ChangePassword failed for username: {username}, reason: {changePasswordResult}", context.UserName, changePasswordResult);
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", "));
await RollbackAndSetInvalidGrantResultAsync(
context,
changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", "));
return;
}
}
else
@ -289,7 +324,7 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
else
{
Logger.LogInformation($"Authentication failed for username: {{{context.UserName}}}, reason: {{{changePasswordType.ToString()}}}");
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, nameof(user.ShouldChangePasswordOnNextLogin),
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, changePasswordType.ToString(),
new Dictionary<string, object>()
{
{"userId", user.Id},
@ -308,6 +343,13 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
protected virtual async Task SetSuccessResultAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user)
{
var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(user);
if (!resetAccessFailedCountResult.Succeeded)
{
await RollbackAndSetInvalidGrantResultAsync(context, Localizer["InvalidUserNameOrPassword"]);
return;
}
var sub = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("Credentials validated for username: {username}", context.UserName);
@ -335,6 +377,17 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
);
}
protected virtual async Task RollbackAndSetInvalidGrantResultAsync(ResourceOwnerPasswordValidationContext context, string errorDescription)
{
var currentUnitOfWork = UnitOfWorkManager.Current;
if (currentUnitOfWork != null)
{
await currentUnitOfWork.RollbackAsync();
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, errorDescription);
}
protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds(ResourceOwnerPasswordValidationContext context)
{
if (!ValidationHelper.IsValidEmailAddress(context.UserName))

1
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo.Abp.IdentityServer.Domain.Tests.csproj

@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.PermissionManagement.Domain.IdentityServer\Volo.Abp.PermissionManagement.Domain.IdentityServer.csproj" />
<ProjectReference Include="..\..\..\identity\src\Volo.Abp.Identity.AspNetCore\Volo.Abp.Identity.AspNetCore.csproj" />
<ProjectReference Include="..\Volo.Abp.IdentityServer.EntityFrameworkCore.Tests\Volo.Abp.IdentityServer.EntityFrameworkCore.Tests.csproj" />
</ItemGroup>

25
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AbpIdentityServerDomainTestModule.cs

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

199
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidatorPasswordChange_Tests.cs

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

174
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidatorTestBase.cs

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

187
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator_Tests.cs

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

37
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/IdentityServerTestSettingValueProvider.cs

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

41
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/IdentityUserStoreFailureSimulator.cs

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

49
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AspNetIdentity/TestIdentityUserStore.cs

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

67
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs

@ -215,13 +215,7 @@ public partial class TokenController
return await SetSuccessResultAsync(request, user);
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Invalid recovery code!"
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return await RollbackAndCreateInvalidGrantResultAsync("Invalid recovery code!");
}
var twoFactorProvider = request.GetParameter("TwoFactorProvider")?.ToString();
@ -234,7 +228,11 @@ public partial class TokenController
return await SetSuccessResultAsync(request, user);
}
await UserManager.AccessFailedAsync(user);
var accessFailedResult = await UserManager.AccessFailedAsync(user);
if (!accessFailedResult.Succeeded)
{
return await RollbackAndCreateInvalidGrantResultAsync("Invalid username or password!");
}
Logger.LogInformation("Authentication failed for username: {username}, reason: InvalidAuthenticatorCode", request.Username);
@ -293,7 +291,16 @@ public partial class TokenController
{
if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordType.ToString(), changePasswordToken))
{
var changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword);
IdentityResult changePasswordResult;
try
{
changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword);
}
catch (AbpIdentityResultException exception)
{
changePasswordResult = exception.IdentityResult;
}
if (changePasswordResult.Succeeded)
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
@ -309,19 +316,25 @@ public partial class TokenController
user.SetShouldChangePasswordOnNextLogin(false);
}
await UserManager.UpdateAsync(user);
var updateUserResult = await UserManager.UpdateAsync(user);
if (!updateUserResult.Succeeded)
{
return await RollbackAndCreateInvalidGrantResultAsync("Invalid username or password!");
}
if (await IsTfaEnabledAsync(user))
{
return await HandleTwoFactorLoginAsync(request, user);
}
return await SetSuccessResultAsync(request, user);
}
else
{
Logger.LogInformation("ChangePassword failed for username: {username}, reason: {changePasswordResult}", request.Username, changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", "));
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", ")
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return await RollbackAndCreateInvalidGrantResultAsync(
changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", "));
}
}
else
@ -387,6 +400,12 @@ public partial class TokenController
protected virtual async Task<IActionResult> SetSuccessResultAsync(OpenIddictRequest request, IdentityUser user)
{
var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(user);
if (!resetAccessFailedCountResult.Succeeded)
{
return await RollbackAndCreateInvalidGrantResultAsync("Invalid username or password!");
}
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
@ -421,6 +440,22 @@ public partial class TokenController
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
protected virtual async Task<IActionResult> RollbackAndCreateInvalidGrantResultAsync(string errorDescription)
{
if (CurrentUnitOfWork != null)
{
await CurrentUnitOfWork.RollbackAsync();
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = errorDescription
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
protected virtual async Task UpdateUserLastSignInTimeAsync(IdentityUser user)
{
await UserManager.UpdateLastSignInTimeAsync(user.Id);

2
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo.Abp.OpenIddict.AspNetCore.Tests.csproj

@ -19,6 +19,8 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.OpenIddict.AspNetCore\Volo.Abp.OpenIddict.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.OpenIddict.EntityFrameworkCore\Volo.Abp.OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\identity\src\Volo.Abp.Identity.AspNetCore\Volo.Abp.Identity.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\identity\src\Volo.Abp.Identity.EntityFrameworkCore\Volo.Abp.Identity.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.TestBase\Volo.Abp.AspNetCore.TestBase.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />

41
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/IdentityUserStoreFailureSimulator.cs

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

114
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantIntegrationTestBase.cs

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

270
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantPasswordChange_Integration_Tests.cs

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

8
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantTestData.cs

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

177
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrantTwoFactor_Integration_Tests.cs

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

25
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictPasswordGrant_Integration_Tests.cs

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

37
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTestSettingValueProvider.cs

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

55
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs

@ -2,33 +2,40 @@ using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.AspNetCore.Uow;
using Volo.Abp.Autofac;
using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Identity.EntityFrameworkCore;
using Volo.Abp.Modularity;
using Volo.Abp.OpenIddict.Applications;
using Volo.Abp.OpenIddict.EntityFrameworkCore;
using Volo.Abp.OpenIddict.Tokens;
using Volo.Abp.SecurityLog;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
using Volo.Abp.Autofac;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace Volo.Abp.OpenIddict.Integration;
public class TokenVisibilityRecorder
{
public long? TokenCountAtResponseStart { get; set; }
}
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpIdentityAspNetCoreModule),
typeof(AbpIdentityEntityFrameworkCoreModule),
typeof(AbpOpenIddictAspNetCoreModule),
typeof(AbpOpenIddictEntityFrameworkCoreModule),
typeof(AbpEntityFrameworkCoreSqliteModule),
@ -59,6 +66,15 @@ public class OpenIddictTokenIntegrationTestModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<TokenVisibilityRecorder>();
context.Services.AddSingleton<IdentityUserStoreFailureSimulator>();
context.Services.AddSingleton<OpenIddictTestSettingValueProvider>();
context.Services.Replace(ServiceDescriptor.Scoped<IdentityUserStore, TestIdentityUserStore>());
Configure<AbpSecurityLogOptions>(options => options.IsEnabled = false);
Configure<AbpSettingOptions>(options =>
{
options.ValueProviders.Add<OpenIddictTestSettingValueProvider>();
});
// A remapped token endpoint, so the tests can prove the opt-in list is derived from the configured
// server endpoints (custom endpoints are followed) rather than a hardcoded "/connect" prefix.
@ -77,6 +93,12 @@ public class OpenIddictTokenIntegrationTestModule : AbpModule
dbContext.Database.EnsureCreated();
}
using (var dbContext = new IdentityDbContext(
new DbContextOptionsBuilder<IdentityDbContext>().UseSqlite(ConnectionString).Options))
{
dbContext.GetService<IRelationalDatabaseCreator>().CreateTables();
}
Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = ConnectionString;
@ -90,7 +112,7 @@ public class OpenIddictTokenIntegrationTestModule : AbpModule
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
SeedClientAsync(context.ServiceProvider).GetAwaiter().GetResult();
SeedDataAsync(context.ServiceProvider).GetAwaiter().GetResult();
var app = context.GetApplicationBuilder();
app.UseRouting();
@ -129,7 +151,7 @@ public class OpenIddictTokenIntegrationTestModule : AbpModule
}
}
private static async Task SeedClientAsync(IServiceProvider serviceProvider)
private static async Task SeedDataAsync(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
@ -147,11 +169,26 @@ public class OpenIddictTokenIntegrationTestModule : AbpModule
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.GrantTypes.Password
}
});
}
var userManager = scope.ServiceProvider.GetRequiredService<IdentityUserManager>();
if (await userManager.FindByNameAsync(OpenIddictPasswordGrantTestData.UserName) == null)
{
var user = new IdentityUser(
Guid.NewGuid(),
OpenIddictPasswordGrantTestData.UserName,
OpenIddictPasswordGrantTestData.Email);
user.SetEmailConfirmed(true);
(await userManager.CreateAsync(user, OpenIddictPasswordGrantTestData.Password)).CheckErrors();
(await userManager.SetLockoutEnabledAsync(user, true)).CheckErrors();
(await userManager.SetTwoFactorEnabledAsync(user, true)).CheckErrors();
}
await uow.CompleteAsync();
}
}

49
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/TestIdentityUserStore.cs

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

6
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/TokenVisibilityRecorder.cs

@ -0,0 +1,6 @@
namespace Volo.Abp.OpenIddict.Integration;
public class TokenVisibilityRecorder
{
public long? TokenCountAtResponseStart { get; set; }
}
Loading…
Cancel
Save