1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-26 12:55:17 +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.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -28,6 +29,7 @@ public class TwoFactorController : Controller
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
public TwoFactorController( public TwoFactorController(
IUserService userService, IUserService userService,
@ -35,7 +37,8 @@ public class TwoFactorController : Controller
IOrganizationService organizationService, IOrganizationService organizationService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
UserManager<User> userManager, UserManager<User> userManager,
ICurrentContext currentContext) ICurrentContext currentContext,
IVerifyAuthRequestCommand verifyAuthRequestCommand)
{ {
_userService = userService; _userService = userService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -43,6 +46,7 @@ public class TwoFactorController : Controller
_globalSettings = globalSettings; _globalSettings = globalSettings;
_userManager = userManager; _userManager = userManager;
_currentContext = currentContext; _currentContext = currentContext;
_verifyAuthRequestCommand = verifyAuthRequestCommand;
} }
[HttpGet("")] [HttpGet("")]
@ -285,21 +289,29 @@ public class TwoFactorController : Controller
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant()); var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
if (user != null) 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 (await _verifyAuthRequestCommand
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null .VerifyAuthRequestAsync(model.AuthRequestId, model.AuthRequestAccessCode))
&&
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
{ {
model.ToUser(user); var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
isBecauseNewDeviceLogin = true;
}
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin); await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return; return;
} }
} }
else
{
if (await _userService.VerifySecretAsync(user, model.Secret))
{
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return;
}
}
}
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException("Cannot send two-factor email."); throw new BadRequestException("Cannot send two-factor email.");
@ -455,4 +467,17 @@ public class TwoFactorController : Controller
await Task.Delay(500); 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)] [StringLength(300)]
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
public string OTP { get; set; } public string OTP { get; set; }
public string AuthRequestAccessCode { get; set; }
public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP; public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 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 string DeviceIdentifier { get; set; }
public Guid AuthRequestId { get; set; }
public User ToUser(User extistingUser) public User ToUser(User extistingUser)
{ {
var providers = extistingUser.GetTwoFactorProviders(); 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) if (authRequest != null)
{ {
var requestAge = DateTime.UtcNow - authRequest.CreationDate; 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)) CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
{ {
authRequest.AuthenticationDate = DateTime.UtcNow; authRequest.AuthenticationDate = DateTime.UtcNow;
@ -123,15 +123,13 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
} }
return false; return false;
} }
else
{
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password)) if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
{ {
return false; return false;
} }
return true; return true;
} }
}
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user, protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
List<Claim> claims, Dictionary<string, object> customResponse) List<Claim> claims, Dictionary<string, object> customResponse)

View File

@ -7,6 +7,7 @@ using Bit.Core.Enums;
using Bit.Core.HostedServices; using Bit.Core.HostedServices;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.LoginFeatures;
using Bit.Core.Models.Business.Tokenables; using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures; using Bit.Core.OrganizationFeatures;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -108,6 +109,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IAppleIapService, AppleIapService>(); services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddScoped<ISsoConfigService, SsoConfigService>(); services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISendService, SendService>(); services.AddScoped<ISendService, SendService>();
services.AddLoginServices();
} }
public static void AddTokenizers(this IServiceCollection services) public static void AddTokenizers(this IServiceCollection services)