1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-27 03:41:30 +01:00

feat(2FA): [PM-17129] Login with 2FA Recovery Code

* feat(2FA): [PM-17129] Login with 2FA Recovery Code - Login with Recovery Code working.

* feat(2FA): [PM-17129] Login with 2FA Recovery Code - Feature flagged implementation.

* style(2FA): [PM-17129] Login with 2FA Recovery Code - Code cleanup.

* test(2FA): [PM-17129] Login with 2FA Recovery Code - Tests.
This commit is contained in:
Patrick-Pimentel-Bitwarden 2025-02-13 15:51:36 -05:00 committed by GitHub
parent 465549b812
commit ac6bc40d85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 220 additions and 76 deletions

View File

@ -304,7 +304,7 @@ public class TwoFactorController : Controller
if (user != null)
{
// check if 2FA email is from passwordless
// Check if 2FA email is from Passwordless.
if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))
{
if (await _verifyAuthRequestCommand
@ -317,17 +317,14 @@ public class TwoFactorController : Controller
}
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
{
if (this.ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
{
await _userService.SendTwoFactorEmailAsync(user);
return;
}
else
{
await this.ThrowDelayedBadRequestExceptionAsync(
"Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.",
2000);
}
await ThrowDelayedBadRequestExceptionAsync(
"Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.");
}
else if (await _userService.VerifySecretAsync(user, requestModel.Secret))
{
@ -336,8 +333,7 @@ public class TwoFactorController : Controller
}
}
await this.ThrowDelayedBadRequestExceptionAsync(
"Cannot send two-factor email.", 2000);
await ThrowDelayedBadRequestExceptionAsync("Cannot send two-factor email.");
}
[HttpPut("email")]
@ -374,7 +370,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
[FromBody] TwoFactorProviderRequestModel model)
{
var user = await CheckAsync(model, false);
await CheckAsync(model, false);
var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -401,6 +397,10 @@ public class TwoFactorController : Controller
return response;
}
/// <summary>
/// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.
/// </summary>
[Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")]
[HttpPost("recover")]
[AllowAnonymous]
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
@ -463,10 +463,8 @@ public class TwoFactorController : Controller
await Task.Delay(2000);
throw new BadRequestException(name, $"{name} is invalid.");
}
else
{
await Task.Delay(500);
}
await Task.Delay(500);
}
private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user)

View File

@ -10,4 +10,5 @@ public enum TwoFactorProviderType : byte
Remember = 5,
OrganizationDuo = 6,
WebAuthn = 7,
RecoveryCode = 8,
}

View File

@ -170,6 +170,7 @@ public static class FeatureFlagKeys
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
public const string AndroidMutualTls = "mutual-tls";
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
public static List<string> GetAllKeys()

View File

@ -22,7 +22,6 @@ public interface IUserService
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
Task SendMasterPasswordHintAsync(string email);
Task SendTwoFactorEmailAsync(User user);
Task<bool> VerifyTwoFactorEmailAsync(User user, string token);
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
@ -41,8 +40,6 @@ public interface IUserService
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type);
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
Task<string> GenerateUserTokenAsync(User user, string tokenProvider, string purpose);
Task<IdentityResult> DeleteAsync(User user);
Task<IdentityResult> DeleteAsync(User user, string token);
Task SendDeleteConfirmationAsync(string email);
@ -55,9 +52,7 @@ public interface IUserService
Task CancelPremiumAsync(User user, bool? endOfPeriod = null);
Task ReinstatePremiumAsync(User user);
Task EnablePremiumAsync(Guid userId, DateTime? expirationDate);
Task EnablePremiumAsync(User user, DateTime? expirationDate);
Task DisablePremiumAsync(Guid userId, DateTime? expirationDate);
Task DisablePremiumAsync(User user, DateTime? expirationDate);
Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate);
Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
int? version = null);
@ -91,9 +86,26 @@ public interface IUserService
void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true);
[Obsolete("To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.")]
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
/// <summary>
/// Returns true if the user is a legacy user. Legacy users use their master key as their encryption key.
/// We force these users to the web to migrate their encryption scheme.
/// This method is used by the TwoFactorAuthenticationValidator to recover two
/// factor for a user. This allows users to be logged in after a successful recovery
/// attempt.
///
/// This method logs the event, sends an email to the user, and removes two factor
/// providers on the user account. This means that a user will have to accomplish
/// new device verification on their account on new logins, if it is enabled for their user.
/// </summary>
/// <param name="recoveryCode">recovery code associated with the user logging in</param>
/// <param name="user">The user to refresh the 2FA and Recovery Code on.</param>
/// <returns>true if the recovery code is valid; false otherwise</returns>
Task<bool> RecoverTwoFactorAsync(User user, string recoveryCode);
/// <summary>
/// Returns true if the user is a legacy user. Legacy users use their master key as their
/// encryption key. We force these users to the web to migrate their encryption scheme.
/// </summary>
Task<bool> IsLegacyUser(string userId);
@ -101,7 +113,8 @@ public interface IUserService
/// Indicates if the user is managed by any organization.
/// </summary>
/// <remarks>
/// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it.
/// A user is considered managed by an organization if their email domain matches one of the
/// verified domains of that organization, and the user is a member of it.
/// The organization must be enabled and able to have verified domains.
/// </remarks>
/// <returns>

View File

@ -315,7 +315,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return;
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
var token = await GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
}
@ -868,6 +868,10 @@ public class UserService : UserManager<User>, IUserService, IDisposable
}
}
/// <summary>
/// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.
/// </summary>
[Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")]
public async Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode)
{
var user = await _userRepository.GetByEmailAsync(email);
@ -897,6 +901,25 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return true;
}
public async Task<bool> RecoverTwoFactorAsync(User user, string recoveryCode)
{
if (!CoreHelpers.FixedTimeEquals(
user.TwoFactorRecoveryCode,
recoveryCode.Replace(" ", string.Empty).Trim().ToLower()))
{
return false;
}
user.TwoFactorProviders = null;
user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
await SaveUserAsync(user);
await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress);
await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa);
await CheckPoliciesOnTwoFactorRemovalAsync(user);
return true;
}
public async Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license,
TaxInfo taxInfo)
@ -1081,7 +1104,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await EnablePremiumAsync(user, expirationDate);
}
public async Task EnablePremiumAsync(User user, DateTime? expirationDate)
private async Task EnablePremiumAsync(User user, DateTime? expirationDate)
{
if (user != null && !user.Premium && user.Gateway.HasValue)
{
@ -1098,7 +1121,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await DisablePremiumAsync(user, expirationDate);
}
public async Task DisablePremiumAsync(User user, DateTime? expirationDate)
private async Task DisablePremiumAsync(User user, DateTime? expirationDate)
{
if (user != null && user.Premium)
{

View File

@ -77,7 +77,7 @@ public abstract class BaseRequestValidator<T> where T : class
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
CustomValidatorRequestContext validatorContext)
{
// 1. we need to check if the user is a bot and if their master password hash is correct
// 1. We need to check if the user is a bot and if their master password hash is correct.
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User;
@ -99,7 +99,7 @@ public abstract class BaseRequestValidator<T> where T : class
return;
}
// 2. Does this user belong to an organization that requires SSO
// 2. Decide if this user belongs to an organization that requires SSO.
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
if (validatorContext.SsoRequired)
{
@ -111,17 +111,22 @@ public abstract class BaseRequestValidator<T> where T : class
return;
}
// 3. Check if 2FA is required
(validatorContext.TwoFactorRequired, var twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
// This flag is used to determine if the user wants a rememberMe token sent when authentication is successful
// 3. Check if 2FA is required.
(validatorContext.TwoFactorRequired, var twoFactorOrganization) =
await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
// This flag is used to determine if the user wants a rememberMe token sent when
// authentication is successful.
var returnRememberMeToken = false;
if (validatorContext.TwoFactorRequired)
{
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
var twoFactorToken = request.Raw["TwoFactorToken"];
var twoFactorProvider = request.Raw["TwoFactorProvider"];
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
!string.IsNullOrWhiteSpace(twoFactorProvider);
// response for 2FA required and not provided state
// 3a. Response for 2FA required and not provided state.
if (!validTwoFactorRequest ||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
{
@ -133,26 +138,27 @@ public abstract class BaseRequestValidator<T> where T : class
return;
}
// Include Master Password Policy in 2FA response
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
// Include Master Password Policy in 2FA response.
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
SetTwoFactorResult(context, resultDict);
return;
}
var twoFactorTokenValid = await _twoFactorAuthenticationValidator
.VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
var twoFactorTokenValid =
await _twoFactorAuthenticationValidator
.VerifyTwoFactorAsync(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
// response for 2FA required but request is not valid or remember token expired state
// 3b. Response for 2FA required but request is not valid or remember token expired state.
if (!twoFactorTokenValid)
{
// The remember me token has expired
// The remember me token has expired.
if (twoFactorProviderType == TwoFactorProviderType.Remember)
{
var resultDict = await _twoFactorAuthenticationValidator
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
// Include Master Password Policy in 2FA response
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
SetTwoFactorResult(context, resultDict);
}
else
@ -163,17 +169,19 @@ public abstract class BaseRequestValidator<T> where T : class
return;
}
// When the two factor authentication is successful, we can check if the user wants a rememberMe token
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
if (twoFactorRemember // Check if the user wants a rememberMe token
&& twoFactorTokenValid // Make sure two factor authentication was successful
&& twoFactorProviderType != TwoFactorProviderType.Remember) // if the two factor auth was rememberMe do not send another token
// 3c. When the 2FA authentication is successful, we can check if the user wants a
// rememberMe token.
var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1";
// Check if the user wants a rememberMe token.
if (twoFactorRemember
// if the 2FA auth was rememberMe do not send another token.
&& twoFactorProviderType != TwoFactorProviderType.Remember)
{
returnRememberMeToken = true;
}
}
// 4. Check if the user is logging in from a new device
// 4. Check if the user is logging in from a new device.
var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext);
if (!deviceValid)
{
@ -182,7 +190,7 @@ public abstract class BaseRequestValidator<T> where T : class
return;
}
// 5. Force legacy users to the web for migration
// 5. Force legacy users to the web for migration.
if (UserService.IsLegacyUser(user) && request.ClientId != "web")
{
await FailAuthForLegacyUserAsync(user, context);
@ -224,7 +232,7 @@ public abstract class BaseRequestValidator<T> where T : class
customResponse.Add("Key", user.Key);
}
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
@ -403,7 +411,7 @@ public abstract class BaseRequestValidator<T> where T : class
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
}
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicy(User user)
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)
{
// Check current context/cache to see if user is in any organizations, avoids extra DB call if not
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders;
@ -44,7 +45,7 @@ public interface ITwoFactorAuthenticationValidator
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
/// <returns>boolean</returns>
Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
Task<bool> VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
}
public class TwoFactorAuthenticationValidator(
@ -139,7 +140,7 @@ public class TwoFactorAuthenticationValidator(
return twoFactorResultDict;
}
public async Task<bool> VerifyTwoFactor(
public async Task<bool> VerifyTwoFactorAsync(
User user,
Organization organization,
TwoFactorProviderType type,
@ -154,24 +155,39 @@ public class TwoFactorAuthenticationValidator(
return false;
}
switch (type)
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin))
{
case TwoFactorProviderType.Authenticator:
case TwoFactorProviderType.Email:
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.YubiKey:
case TwoFactorProviderType.WebAuthn:
case TwoFactorProviderType.Remember:
if (type != TwoFactorProviderType.Remember &&
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return false;
}
return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type), token);
default:
return false;
if (type is TwoFactorProviderType.RecoveryCode)
{
return await _userService.RecoverTwoFactorAsync(user, token);
}
}
// These cases we want to always return false, U2f is deprecated and OrganizationDuo
// uses a different flow than the other two factor providers, it follows the same
// structure of a UserTokenProvider but has it's logic ran outside the usual token
// provider flow. See IOrganizationDuoUniversalTokenProvider.cs
if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo)
{
return false;
}
// Now we are concerning the rest of the Two Factor Provider Types
// The intent of this check is to make sure that the user is using a 2FA provider that
// is enabled and allowed by their premium status. The exception for Remember
// is because it is a "special" 2FA type that isn't ever explicitly
// enabled by a user, so we can't check the user's 2FA providers to see if they're
// enabled. We just have to check if the token is valid.
if (type != TwoFactorProviderType.Remember &&
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return false;
}
// Finally, verify the token based on the provider type.
return await _userManager.VerifyTwoFactorTokenAsync(
user, CoreHelpers.CustomProviderName(type), token);
}
private async Task<List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>> GetEnabledTwoFactorProvidersAsync(

View File

@ -730,6 +730,46 @@ public class UserServiceTests
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RecoverTwoFactorAsync_CorrectCode_ReturnsTrueAndProcessesPolicies(
User user, SutProvider<UserService> sutProvider)
{
// Arrange
var recoveryCode = "1234";
user.TwoFactorRecoveryCode = recoveryCode;
// Act
var response = await sutProvider.Sut.RecoverTwoFactorAsync(user, recoveryCode);
// Assert
Assert.True(response);
Assert.Null(user.TwoFactorProviders);
// Make sure a new code was generated for the user
Assert.NotEqual(recoveryCode, user.TwoFactorRecoveryCode);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendRecoverTwoFactorEmail(Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_Recovered2fa);
}
[Theory, BitAutoData]
public async Task RecoverTwoFactorAsync_IncorrectCode_ReturnsFalse(
User user, SutProvider<UserService> sutProvider)
{
// Arrange
var recoveryCode = "1234";
user.TwoFactorRecoveryCode = "4567";
// Act
var response = await sutProvider.Sut.RecoverTwoFactorAsync(user, recoveryCode);
// Assert
Assert.False(response);
Assert.NotNull(user.TwoFactorProviders);
}
private static void SetupUserAndDevice(User user,
bool shouldHavePassword)
{

View File

@ -105,7 +105,7 @@ public class BaseRequestValidatorTests
// Assert
await _eventService.Received(1)
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id,
Core.Enums.EventType.User_FailedLogIn);
EventType.User_FailedLogIn);
Assert.True(context.GrantResult.IsError);
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.Models.Business.Tokenables;
@ -328,7 +329,7 @@ public class TwoFactorAuthenticationValidatorTests
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
// Act
var result = await _sut.VerifyTwoFactor(
var result = await _sut.VerifyTwoFactorAsync(
user, null, TwoFactorProviderType.U2f, token);
// Assert
@ -348,7 +349,7 @@ public class TwoFactorAuthenticationValidatorTests
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
// Act
var result = await _sut.VerifyTwoFactor(
var result = await _sut.VerifyTwoFactorAsync(
user, null, TwoFactorProviderType.Email, token);
// Assert
@ -368,7 +369,7 @@ public class TwoFactorAuthenticationValidatorTests
_userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"];
// Act
var result = await _sut.VerifyTwoFactor(
var result = await _sut.VerifyTwoFactorAsync(
user, null, TwoFactorProviderType.OrganizationDuo, token);
// Assert
@ -394,7 +395,7 @@ public class TwoFactorAuthenticationValidatorTests
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
// Act
var result = await _sut.VerifyTwoFactor(user, null, providerType, token);
var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token);
// Assert
Assert.True(result);
@ -419,7 +420,7 @@ public class TwoFactorAuthenticationValidatorTests
_userManager.TWO_FACTOR_TOKEN_VERIFIED = false;
// Act
var result = await _sut.VerifyTwoFactor(user, null, providerType, token);
var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token);
// Assert
Assert.False(result);
@ -445,13 +446,56 @@ public class TwoFactorAuthenticationValidatorTests
organization.Enabled = true;
// Act
var result = await _sut.VerifyTwoFactor(
var result = await _sut.VerifyTwoFactorAsync(
user, organization, providerType, token);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.RecoveryCode)]
public async void VerifyTwoFactorAsync_RecoveryCode_ValidToken_ReturnsTrue(
TwoFactorProviderType providerType,
User user,
Organization organization)
{
var token = "1234";
user.TwoFactorRecoveryCode = token;
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true);
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
// Act
var result = await _sut.VerifyTwoFactorAsync(
user, organization, providerType, token);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.RecoveryCode)]
public async void VerifyTwoFactorAsync_RecoveryCode_InvalidToken_ReturnsFalse(
TwoFactorProviderType providerType,
User user,
Organization organization)
{
// Arrange
var token = "1234";
user.TwoFactorRecoveryCode = token;
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false);
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
// Act
var result = await _sut.VerifyTwoFactorAsync(
user, organization, providerType, token);
// Assert
Assert.False(result);
}
private static UserManagerTestWrapper<User> SubstituteUserManager()
{
return new UserManagerTestWrapper<User>(