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:
parent
864ab5231d
commit
4a26c55599
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
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)
|
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)
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user