diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index b07f65c4f..76daf18e4 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -6,6 +6,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -28,6 +29,7 @@ public class TwoFactorController : Controller private readonly GlobalSettings _globalSettings; private readonly UserManager _userManager; private readonly ICurrentContext _currentContext; + private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand; public TwoFactorController( IUserService userService, @@ -35,7 +37,8 @@ public class TwoFactorController : Controller IOrganizationService organizationService, GlobalSettings globalSettings, UserManager userManager, - ICurrentContext currentContext) + ICurrentContext currentContext, + IVerifyAuthRequestCommand verifyAuthRequestCommand) { _userService = userService; _organizationRepository = organizationRepository; @@ -43,6 +46,7 @@ public class TwoFactorController : Controller _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; + _verifyAuthRequestCommand = verifyAuthRequestCommand; } [HttpGet("")] @@ -285,19 +289,27 @@ public class TwoFactorController : Controller var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant()); if (user != null) { - if (await _userService.VerifySecretAsync(user, model.Secret)) + // check if 2FA email is from passwordless + if (!string.IsNullOrEmpty(model.AuthRequestAccessCode)) { - var isBecauseNewDeviceLogin = false; - if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null - && - await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null)) + if (await _verifyAuthRequestCommand + .VerifyAuthRequestAsync(model.AuthRequestId, model.AuthRequestAccessCode)) { - model.ToUser(user); - isBecauseNewDeviceLogin = true; - } + var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model); - await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin); - return; + await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin); + return; + } + } + else + { + if (await _userService.VerifySecretAsync(user, model.Secret)) + { + var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model); + + await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin); + return; + } } } @@ -455,4 +467,17 @@ public class TwoFactorController : Controller await Task.Delay(500); } } + + private async Task IsNewDeviceLoginAsync(User user, TwoFactorEmailRequestModel model) + { + if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null + && + await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null)) + { + model.ToUser(user); + return true; + } + + return false; + } } diff --git a/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs b/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs index f35ea9677..886ad16cd 100644 --- a/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs +++ b/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs @@ -7,13 +7,14 @@ public class SecretVerificationRequestModel : IValidatableObject [StringLength(300)] public string MasterPasswordHash { get; set; } public string OTP { get; set; } + public string AuthRequestAccessCode { get; set; } public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP; public virtual IEnumerable Validate(ValidationContext validationContext) { - if (string.IsNullOrEmpty(Secret)) + if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode)) { - yield return new ValidationResult("MasterPasswordHash or OTP must be supplied."); + yield return new ValidationResult("MasterPasswordHash, OTP or AccessCode must be supplied."); } } } diff --git a/src/Api/Models/Request/TwoFactorRequestModels.cs b/src/Api/Models/Request/TwoFactorRequestModels.cs index 3ce42cdb9..d7ceeb2f8 100644 --- a/src/Api/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Models/Request/TwoFactorRequestModels.cs @@ -204,6 +204,8 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel public string DeviceIdentifier { get; set; } + public Guid AuthRequestId { get; set; } + public User ToUser(User extistingUser) { var providers = extistingUser.GetTwoFactorProviders(); diff --git a/src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs b/src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs new file mode 100644 index 000000000..13cf4e3be --- /dev/null +++ b/src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using Bit.Core.LoginFeatures.PasswordlessLogin; +using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.LoginFeatures; + +public static class LoginServiceCollectionExtensions +{ + public static void AddLoginServices(this IServiceCollection services) + { + services.AddScoped(); + } +} + diff --git a/src/Core/LoginFeatures/PasswordlessLogin/Interfaces/IVerifyAuthRequest.cs b/src/Core/LoginFeatures/PasswordlessLogin/Interfaces/IVerifyAuthRequest.cs new file mode 100644 index 000000000..36809e7c3 --- /dev/null +++ b/src/Core/LoginFeatures/PasswordlessLogin/Interfaces/IVerifyAuthRequest.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces; + +public interface IVerifyAuthRequestCommand +{ + Task VerifyAuthRequestAsync(Guid authRequestId, string accessCode); +} diff --git a/src/Core/LoginFeatures/PasswordlessLogin/VerifyAuthRequest.cs b/src/Core/LoginFeatures/PasswordlessLogin/VerifyAuthRequest.cs new file mode 100644 index 000000000..67fc7268d --- /dev/null +++ b/src/Core/LoginFeatures/PasswordlessLogin/VerifyAuthRequest.cs @@ -0,0 +1,24 @@ +using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.LoginFeatures.PasswordlessLogin; + +public class VerifyAuthRequestCommand : IVerifyAuthRequestCommand +{ + private readonly IAuthRequestRepository _authRequestRepository; + + public VerifyAuthRequestCommand(IAuthRequestRepository authRequestRepository) + { + _authRequestRepository = authRequestRepository; + } + + public async Task VerifyAuthRequestAsync(Guid authRequestId, string accessCode) + { + var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId); + if (authRequest == null || authRequest.AccessCode != accessCode) + { + return false; + } + return true; + } +} diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index 278baef06..65e676c4d 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -113,7 +113,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator(); services.AddScoped(); services.AddScoped(); + services.AddLoginServices(); } public static void AddTokenizers(this IServiceCollection services)