mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[PM-8220] New Device Verification (#5084)
* feat(BaseRequestValidator): Add global setting for new device verification. Refactor BaseRequestValidator enabling better self-documenting code and better single responsibility principle for validators. Updated DeviceValidator to handle new device verification, behind a feature flag. Moved IDeviceValidator interface to separate file. Updated CustomRequestValidator to act as the conduit by which *Validators communicate authentication context between themselves and the RequestValidators. Adding new test for DeviceValidator class. Updated tests for BaseRequestValidator as some functionality was moved to the DeviceValidator class.
This commit is contained in:
@ -1,9 +1,12 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
@ -20,6 +23,8 @@ public class DeviceValidatorTests
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly DeviceValidator _sut;
|
||||
|
||||
public DeviceValidatorTests()
|
||||
@ -29,219 +34,550 @@ public class DeviceValidatorTests
|
||||
_globalSettings = new GlobalSettings();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_sut = new DeviceValidator(
|
||||
_deviceService,
|
||||
_deviceRepository,
|
||||
_globalSettings,
|
||||
_mailService,
|
||||
_currentContext);
|
||||
_currentContext,
|
||||
_userService,
|
||||
_featureService);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void SaveDeviceAsync_DeviceNull_ShouldReturnNull(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
request.Raw["DeviceIdentifier"] = null;
|
||||
|
||||
// Act
|
||||
var device = await _sut.SaveDeviceAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.Null(device);
|
||||
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void SaveDeviceAsync_UserIsNull_ShouldReturnNull(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
|
||||
// Act
|
||||
var device = await _sut.SaveDeviceAsync(null, request);
|
||||
|
||||
// Assert
|
||||
Assert.Null(device);
|
||||
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendsEmail(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
|
||||
user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
// Act
|
||||
var device = await _sut.SaveDeviceAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(device);
|
||||
Assert.Equal(user.Id, device.UserId);
|
||||
Assert.Equal("DeviceIdentifier", device.Identifier);
|
||||
Assert.Equal(DeviceType.Android, device.Type);
|
||||
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
|
||||
user.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendEmailFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
|
||||
user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
|
||||
_globalSettings.DisableEmailNewDevice = true;
|
||||
|
||||
// Act
|
||||
var device = await _sut.SaveDeviceAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(device);
|
||||
Assert.Equal(user.Id, device.UserId);
|
||||
Assert.Equal("DeviceIdentifier", device.Identifier);
|
||||
Assert.Equal(DeviceType.Android, device.Type);
|
||||
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
|
||||
user.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void SaveDeviceAsync_DeviceIsKnown_ShouldReturnDevice(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user,
|
||||
[Theory, BitAutoData]
|
||||
public async void GetKnownDeviceAsync_UserNull_ReturnsFalse(
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
|
||||
device.UserId = user.Id;
|
||||
device.Identifier = "DeviceIdentifier";
|
||||
device.Type = DeviceType.Android;
|
||||
device.Name = "DeviceName";
|
||||
device.PushToken = "DevicePushToken";
|
||||
_deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id).Returns(device);
|
||||
// AutoData arrages
|
||||
|
||||
// Act
|
||||
var resultDevice = await _sut.SaveDeviceAsync(user, request);
|
||||
var result = await _sut.GetKnownDeviceAsync(null, device);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(device, resultDevice);
|
||||
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void SaveDeviceAsync_NewUser_DeviceUnknown_ShouldSaveDevice_NoEmail(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
user.CreationDate = DateTime.UtcNow;
|
||||
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>()).Returns(null as Device);
|
||||
|
||||
// Act
|
||||
var device = await _sut.SaveDeviceAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(device);
|
||||
Assert.Equal(user.Id, device.UserId);
|
||||
Assert.Equal("DeviceIdentifier", device.Identifier);
|
||||
Assert.Equal(DeviceType.Android, device.Type);
|
||||
await _deviceService.Received(1).SaveAsync(device);
|
||||
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void KnownDeviceAsync_UserNull_ReturnsFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
|
||||
// Act
|
||||
var result = await _sut.KnownDeviceAsync(null, request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void KnownDeviceAsync_DeviceNull_ReturnsFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
[Theory, BitAutoData]
|
||||
public async void GetKnownDeviceAsync_DeviceNull_ReturnsFalse(
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
// Device raw data is null which will cause the device to be null
|
||||
|
||||
// Act
|
||||
var result = await _sut.KnownDeviceAsync(user, request);
|
||||
var result = await _sut.GetKnownDeviceAsync(user, null);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void KnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
[Theory, BitAutoData]
|
||||
public async void GetKnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse(
|
||||
User user,
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
|
||||
.Returns(null as Device);
|
||||
// Act
|
||||
var result = await _sut.KnownDeviceAsync(user, request);
|
||||
var result = await _sut.GetKnownDeviceAsync(user, device);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void KnownDeviceAsync_UserAndDeviceValid_ReturnsTrue(
|
||||
[Theory, BitAutoData]
|
||||
public async void GetKnownDeviceAsync_UserAndDeviceValid_ReturnsTrue(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user,
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
request = AddValidDeviceToRequest(request);
|
||||
AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
|
||||
.Returns(device);
|
||||
// Act
|
||||
var result = await _sut.KnownDeviceAsync(user, request);
|
||||
var result = await _sut.GetKnownDeviceAsync(user, device);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("not null", "Android", "")]
|
||||
[BitAutoData("not null", "", "not null")]
|
||||
[BitAutoData("", "Android", "not null")]
|
||||
public void GetDeviceFromRequest_RawDeviceInfoNull_ReturnsNull(
|
||||
string deviceIdentifier,
|
||||
string deviceType,
|
||||
string deviceName,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
request.Raw["DeviceIdentifier"] = deviceIdentifier;
|
||||
request.Raw["DeviceType"] = deviceType;
|
||||
request.Raw["DeviceName"] = deviceName;
|
||||
|
||||
// Act
|
||||
var result = DeviceValidator.GetDeviceFromRequest(request);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GetDeviceFromRequest_RawDeviceInfoValid_ReturnsDevice(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
AddValidDeviceToRequest(request);
|
||||
|
||||
// Act
|
||||
var result = DeviceValidator.GetDeviceFromRequest(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("DeviceIdentifier", result.Identifier);
|
||||
Assert.Equal("DeviceName", result.Name);
|
||||
Assert.Equal(DeviceType.Android, result.Type);
|
||||
Assert.Equal("DevicePushToken", result.PushToken);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_DeviceNull_ContextModified_ReturnsFalse(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
context.Device = null;
|
||||
|
||||
// Act
|
||||
Assert.NotNull(context.User);
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorModel = new ErrorResponseModel("no device information provided");
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorModel.Message, actualResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_RequestDeviceKnown_ContextDeviceModified_ReturnsTrue(
|
||||
Device device,
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
context.Device = null;
|
||||
AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
|
||||
.Returns(device);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||
|
||||
Assert.True(result);
|
||||
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||
Assert.NotNull(context.Device);
|
||||
Assert.Equal(context.Device, device);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_ContextDeviceKnown_ContextDeviceModified_ReturnsTrue(
|
||||
Device databaseDevice,
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
|
||||
.Returns(databaseDevice);
|
||||
// we want to show that the context device is updated when the device is known
|
||||
Assert.NotEqual(context.Device, databaseDevice);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||
|
||||
Assert.True(result);
|
||||
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||
Assert.Equal(context.Device, databaseDevice);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
AddValidDeviceToRequest(request);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(false);
|
||||
// set user creation to more than 10 minutes ago
|
||||
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
AddValidDeviceToRequest(request);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(false);
|
||||
// set user creation to less than 10 minutes ago
|
||||
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
await _mailService.Received(0).SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
AddValidDeviceToRequest(request);
|
||||
_globalSettings.DisableEmailNewDevice = true;
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
await _mailService.Received(0).SendNewDeviceLoggedInEmail(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
[BitAutoData("authorization_code")]
|
||||
[BitAutoData("client_credentials")]
|
||||
public async void ValidateRequestDeviceAsync_GrantTypeNotPassword_SavesDevice_ReturnsTrue(
|
||||
string grantType,
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(true);
|
||||
|
||||
request.GrantType = grantType;
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_IsAuthRequest_SavesDevice_ReturnsTrue(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(true);
|
||||
|
||||
request.Raw.Add("AuthRequest", "authRequest");
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_TwoFactorRequired_SavesDevice_ReturnsTrue(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(true);
|
||||
|
||||
context.TwoFactorRequired = true;
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void ValidateRequestDeviceAsync_SsoRequired_SavesDevice_ReturnsTrue(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
context.KnownDevice = false;
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
AddValidDeviceToRequest(request);
|
||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||
.Returns(null as Device);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
||||
.Returns(true);
|
||||
|
||||
context.SsoRequired = true;
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void HandleNewDeviceVerificationAsync_UserNull_ContextModified_ReturnsInvalidUser(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
|
||||
_globalSettings.EnableNewDeviceVerification = true;
|
||||
|
||||
context.User = null;
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
// PM-13340: The error message should be "invalid user" instead of "no device information provided"
|
||||
var expectedErrorMessage = "no device information provided";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void HandleNewDeviceVerificationAsync_NewDeviceOtpValid_ReturnsSuccess(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
|
||||
_globalSettings.EnableNewDeviceVerification = true;
|
||||
|
||||
var newDeviceOtp = "123456";
|
||||
request.Raw.Add("NewDeviceOtp", newDeviceOtp);
|
||||
|
||||
_userService.VerifyOTPAsync(context.User, newDeviceOtp).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _userService.Received(0).SendOTPAsync(context.User);
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||
Assert.Equal(context.User.Id, context.Device.UserId);
|
||||
Assert.NotNull(context.Device);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("123456")]
|
||||
public async void HandleNewDeviceVerificationAsync_NewDeviceOtpInvalid_ReturnsInvalidNewDeviceOtp(
|
||||
string newDeviceOtp,
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
|
||||
_globalSettings.EnableNewDeviceVerification = true;
|
||||
|
||||
request.Raw.Add("NewDeviceOtp", newDeviceOtp);
|
||||
|
||||
_userService.VerifyOTPAsync(context.User, newDeviceOtp).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _userService.DidNotReceive().SendOTPAsync(Arg.Any<User>());
|
||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorMessage = "invalid new device otp";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void HandleNewDeviceVerificationAsync_UserHasNoDevices_ReturnsSuccess(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
|
||||
_globalSettings.EnableNewDeviceVerification = true;
|
||||
_deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _userService.Received(0).VerifyOTPAsync(Arg.Any<User>(), Arg.Any<string>());
|
||||
await _userService.Received(0).SendOTPAsync(Arg.Any<User>());
|
||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||
Assert.Equal(context.User.Id, context.Device.UserId);
|
||||
Assert.NotNull(context.Device);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void HandleNewDeviceVerificationAsync_NewDeviceOtpEmpty_UserHasDevices_ReturnsNewDeviceVerificationRequired(
|
||||
CustomValidatorRequestContext context,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
|
||||
_globalSettings.EnableNewDeviceVerification = true;
|
||||
_deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||
|
||||
// Assert
|
||||
await _userService.Received(1).SendOTPAsync(context.User);
|
||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorMessage = "new device verification required";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void NewDeviceOtpRequest_NewDeviceOtpNull_ReturnsFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
// Autodata arranges
|
||||
|
||||
// Act
|
||||
var result = DeviceValidator.NewDeviceOtpRequest(request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void NewDeviceOtpRequest_NewDeviceOtpNotNull_ReturnsTrue(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||
{
|
||||
// Arrange
|
||||
request.Raw["NewDeviceOtp"] = "123456";
|
||||
|
||||
// Act
|
||||
var result = DeviceValidator.NewDeviceOtpRequest(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
private ValidatedTokenRequest AddValidDeviceToRequest(ValidatedTokenRequest request)
|
||||
private static void AddValidDeviceToRequest(ValidatedTokenRequest request)
|
||||
{
|
||||
request.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
||||
request.Raw["DeviceType"] = "Android";
|
||||
request.Raw["DeviceType"] = "Android"; // must be valid device type
|
||||
request.Raw["DeviceName"] = "DeviceName";
|
||||
request.Raw["DevicePushToken"] = "DevicePushToken";
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the request context to facilitate testing the HandleNewDeviceVerificationAsync method.
|
||||
/// </summary>
|
||||
/// <param name="context">test context</param>
|
||||
/// <param name="request">test request</param>
|
||||
private static void ArrangeForHandleNewDeviceVerificationTest(
|
||||
CustomValidatorRequestContext context,
|
||||
ValidatedTokenRequest request)
|
||||
{
|
||||
context.KnownDevice = false;
|
||||
request.GrantType = "password";
|
||||
context.TwoFactorRequired = false;
|
||||
context.SsoRequired = false;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user