mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
PS-82 check send 2FA email for new devices on TwoFactorController send-email-login (#1977)
This commit is contained in:
parent
68f875b3d9
commit
a7a45893a3
@ -289,7 +289,16 @@ namespace Bit.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (await _userService.VerifySecretAsync(user, model.Secret))
|
if (await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
var isBecauseNewDeviceLogin = false;
|
||||||
|
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null
|
||||||
|
&&
|
||||||
|
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
|
||||||
|
{
|
||||||
|
model.ToUser(user);
|
||||||
|
isBecauseNewDeviceLogin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,6 +203,8 @@ namespace Bit.Api.Models.Request
|
|||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string DeviceIdentifier { get; set; }
|
||||||
|
|
||||||
public User ToUser(User extistingUser)
|
public User ToUser(User extistingUser)
|
||||||
{
|
{
|
||||||
var providers = extistingUser.GetTwoFactorProviders();
|
var providers = extistingUser.GetTwoFactorProviders();
|
||||||
|
@ -319,9 +319,11 @@ namespace Bit.Core.IdentityServer
|
|||||||
|
|
||||||
var requires2FA = individualRequired || firstEnabledOrg != null;
|
var requires2FA = individualRequired || firstEnabledOrg != null;
|
||||||
var requires2FABecauseNewDevice = !requires2FA
|
var requires2FABecauseNewDevice = !requires2FA
|
||||||
&& user.EmailVerified
|
&&
|
||||||
&& request.GrantType != "authorization_code"
|
await _userService.Needs2FABecauseNewDeviceAsync(
|
||||||
&& await IsNewDeviceAndNotTheFirstOneAsync(user, request);
|
user,
|
||||||
|
GetDeviceFromRequest(request)?.Identifier,
|
||||||
|
request.GrantType);
|
||||||
|
|
||||||
requires2FA = requires2FA || requires2FABecauseNewDevice;
|
requires2FA = requires2FA || requires2FABecauseNewDevice;
|
||||||
|
|
||||||
@ -536,22 +538,6 @@ namespace Bit.Core.IdentityServer
|
|||||||
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
|
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<bool> IsNewDeviceAndNotTheFirstOneAsync(User user, ValidatedTokenRequest request)
|
|
||||||
{
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
|
|
||||||
if (!devices.Any())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !devices.Any(d => d.Identifier == GetDeviceFromRequest(request)?.Identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||||
{
|
{
|
||||||
var device = GetDeviceFromRequest(request);
|
var device = GetDeviceFromRequest(request);
|
||||||
|
@ -78,5 +78,6 @@ namespace Bit.Core.Services
|
|||||||
Task SendOTPAsync(User user);
|
Task SendOTPAsync(User user);
|
||||||
Task<bool> VerifyOTPAsync(User user, string token);
|
Task<bool> VerifyOTPAsync(User user, string token);
|
||||||
Task<bool> VerifySecretAsync(User user, string secret);
|
Task<bool> VerifySecretAsync(User user, string secret);
|
||||||
|
Task<bool> Needs2FABecauseNewDeviceAsync(User user, string deviceIdentifier, string grantType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,9 @@ using Bit.Core.Utilities;
|
|||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
using Fido2NetLib.Objects;
|
using Fido2NetLib.Objects;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using File = System.IO.File;
|
using File = System.IO.File;
|
||||||
@ -51,6 +53,8 @@ namespace Bit.Core.Services
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@ -79,7 +83,9 @@ namespace Bit.Core.Services
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IProviderUserRepository providerUserRepository)
|
IProviderUserRepository providerUserRepository,
|
||||||
|
IDeviceRepository deviceRepository,
|
||||||
|
IWebHostEnvironment environment)
|
||||||
: base(
|
: base(
|
||||||
store,
|
store,
|
||||||
optionsAccessor,
|
optionsAccessor,
|
||||||
@ -114,6 +120,8 @@ namespace Bit.Core.Services
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
|
_deviceRepository = deviceRepository;
|
||||||
|
_environment = environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||||
@ -1408,5 +1416,29 @@ namespace Bit.Core.Services
|
|||||||
? await VerifyOTPAsync(user, secret)
|
? await VerifyOTPAsync(user, secret)
|
||||||
: await CheckPasswordAsync(user, secret);
|
: await CheckPasswordAsync(user, secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Needs2FABecauseNewDeviceAsync(User user, string deviceIdentifier, string grantType)
|
||||||
|
{
|
||||||
|
return user.EmailVerified
|
||||||
|
&& grantType != "authorization_code"
|
||||||
|
&& !_environment.IsDevelopment()
|
||||||
|
&& await IsNewDeviceAndNotTheFirstOneAsync(user, deviceIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsNewDeviceAndNotTheFirstOneAsync(User user, string deviceIdentifier)
|
||||||
|
{
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
if (!devices.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !devices.Any(d => d.Identifier == deviceIdentifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,14 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReceivedExtensions;
|
using NSubstitute.ReceivedExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -156,5 +159,112 @@ namespace Bit.Core.Test.Services
|
|||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
|
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
|
public async Task Needs2FABecauseNewDeviceAsync_ReturnsTrue(SutProvider<UserService> sutProvider, User user)
|
||||||
|
{
|
||||||
|
user.Id = Guid.NewGuid();
|
||||||
|
user.EmailVerified = true;
|
||||||
|
const string deviceIdToCheck = "7b01b586-b210-499f-8d52-0c3fdaa646fc";
|
||||||
|
const string deviceIdInRepo = "ea29126c-91b7-4cc4-8ce6-00105b37f64a";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns(Task.FromResult<ICollection<Device>>(new List<Device>
|
||||||
|
{
|
||||||
|
new Device { Identifier = deviceIdInRepo }
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.True(await sutProvider.Sut.Needs2FABecauseNewDeviceAsync(user, deviceIdToCheck, "password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
|
public async Task Needs2FABecauseNewDeviceAsync_ReturnsFalse_When_GranType_Is_AuthorizationCode(SutProvider<UserService> sutProvider, User user)
|
||||||
|
{
|
||||||
|
user.Id = Guid.NewGuid();
|
||||||
|
user.EmailVerified = true;
|
||||||
|
const string deviceIdToCheck = "7b01b586-b210-499f-8d52-0c3fdaa646fc";
|
||||||
|
const string deviceIdInRepo = "ea29126c-91b7-4cc4-8ce6-00105b37f64a";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns(Task.FromResult<ICollection<Device>>(new List<Device>
|
||||||
|
{
|
||||||
|
new Device { Identifier = deviceIdInRepo }
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.False(await sutProvider.Sut.Needs2FABecauseNewDeviceAsync(user, deviceIdToCheck, "authorization_code"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
|
public async Task Needs2FABecauseNewDeviceAsync_ReturnsFalse_When_Email_Is_Not_Verified(SutProvider<UserService> sutProvider, User user)
|
||||||
|
{
|
||||||
|
user.Id = Guid.NewGuid();
|
||||||
|
user.EmailVerified = false;
|
||||||
|
const string deviceIdToCheck = "7b01b586-b210-499f-8d52-0c3fdaa646fc";
|
||||||
|
const string deviceIdInRepo = "ea29126c-91b7-4cc4-8ce6-00105b37f64a";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns(Task.FromResult<ICollection<Device>>(new List<Device>
|
||||||
|
{
|
||||||
|
new Device { Identifier = deviceIdInRepo }
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.False(await sutProvider.Sut.Needs2FABecauseNewDeviceAsync(user, deviceIdToCheck, "password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
|
public async Task Needs2FABecauseNewDeviceAsync_ReturnsFalse_When_Is_The_First_Device(SutProvider<UserService> sutProvider, User user)
|
||||||
|
{
|
||||||
|
user.Id = Guid.NewGuid();
|
||||||
|
user.EmailVerified = true;
|
||||||
|
const string deviceIdToCheck = "7b01b586-b210-499f-8d52-0c3fdaa646fc";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns(Task.FromResult<ICollection<Device>>(new List<Device>()));
|
||||||
|
|
||||||
|
Assert.False(await sutProvider.Sut.Needs2FABecauseNewDeviceAsync(user, deviceIdToCheck, "password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
|
public async Task Needs2FABecauseNewDeviceAsync_ReturnsFalse_When_DeviceId_Is_Already_In_Repo(SutProvider<UserService> sutProvider, User user)
|
||||||
|
{
|
||||||
|
user.Id = Guid.NewGuid();
|
||||||
|
user.EmailVerified = true;
|
||||||
|
const string deviceIdToCheck = "7b01b586-b210-499f-8d52-0c3fdaa646fc";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns(Task.FromResult<ICollection<Device>>(new List<Device>
|
||||||
|
{
|
||||||
|
new Device { Identifier = deviceIdToCheck }
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.False(await sutProvider.Sut.Needs2FABecauseNewDeviceAsync(user, deviceIdToCheck, "password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
|
public async Task Needs2FABecauseNewDeviceAsync_ReturnsFalse_When_Environment_Is_Development(SutProvider<UserService> sutProvider, User user)
|
||||||
|
{
|
||||||
|
user.Id = Guid.NewGuid();
|
||||||
|
user.EmailVerified = true;
|
||||||
|
const string deviceIdToCheck = "7b01b586-b210-499f-8d52-0c3fdaa646fc";
|
||||||
|
const string deviceIdInRepo = "ea29126c-91b7-4cc4-8ce6-00105b37f64a";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns(Task.FromResult<ICollection<Device>>(new List<Device>
|
||||||
|
{
|
||||||
|
new Device { Identifier = deviceIdInRepo }
|
||||||
|
}));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IWebHostEnvironment>()
|
||||||
|
.EnvironmentName
|
||||||
|
.Returns(Environments.Development);
|
||||||
|
|
||||||
|
Assert.False(await sutProvider.Sut.Needs2FABecauseNewDeviceAsync(user, deviceIdToCheck, "password"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user