mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01:00
[SG-698] Refactored 2fa send email and identity to cater for passwordless (#2346)
* Allow for auth request validation for sending two factor emails * Refactored 2fa send email and identity to cater for passwordless * Refactored 2fa send email and identity to cater for passwordless Signed-off-by: gbubemismith <gsmithwalter@gmail.com> * Inform that we track issues outside of Github (#2331) * Inform that we track issues outside of Github * Use checkboxes for info acknowledgement Signed-off-by: gbubemismith <gsmithwalter@gmail.com> * Refactored 2fa send email and identity to cater for passwordless * ran dotnet format Signed-off-by: gbubemismith <gsmithwalter@gmail.com> Co-authored-by: addison <addisonbeck1@gmail.com>
This commit is contained in:
parent
864ab5231d
commit
4a26c55599
@ -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<User> _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<User> 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<bool> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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<ValidationResult> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
14
src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs
Normal file
14
src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs
Normal file
@ -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<IVerifyAuthRequestCommand, VerifyAuthRequestCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
|
||||
public interface IVerifyAuthRequestCommand
|
||||
{
|
||||
Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode);
|
||||
}
|
@ -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<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
|
||||
if (authRequest == null || authRequest.AccessCode != accessCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -113,7 +113,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
if (authRequest != null)
|
||||
{
|
||||
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
|
||||
if (requestAge < TimeSpan.FromHours(1) && !authRequest.AuthenticationDate.HasValue &&
|
||||
if (requestAge < TimeSpan.FromHours(1) &&
|
||||
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
|
||||
{
|
||||
authRequest.AuthenticationDate = DateTime.UtcNow;
|
||||
@ -123,14 +123,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
|
||||
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
||||
{
|
||||
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.HostedServices;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.LoginFeatures;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.OrganizationFeatures;
|
||||
using Bit.Core.Repositories;
|
||||
@ -108,6 +109,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<IAppleIapService, AppleIapService>();
|
||||
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
||||
services.AddScoped<ISendService, SendService>();
|
||||
services.AddLoginServices();
|
||||
}
|
||||
|
||||
public static void AddTokenizers(this IServiceCollection services)
|
||||
|
Loading…
Reference in New Issue
Block a user