1
0
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:
Gbubemi Smith 2022-10-18 14:50:48 -04:00 committed by GitHub
parent 864ab5231d
commit 4a26c55599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 92 additions and 20 deletions

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,6 @@
namespace Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
public interface IVerifyAuthRequestCommand
{
Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode);
}

View File

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

View File

@ -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,

View File

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