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:
parent
465549b812
commit
ac6bc40d85
@ -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)
|
||||
|
@ -10,4 +10,5 @@ public enum TwoFactorProviderType : byte
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6,
|
||||
WebAuthn = 7,
|
||||
RecoveryCode = 8,
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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))
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>(
|
||||
|
Loading…
Reference in New Issue
Block a user