1
0
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:
Ike
2024-12-12 09:08:11 -08:00
committed by GitHub
parent a76a9cb800
commit 867fa848dd
15 changed files with 1112 additions and 473 deletions

View File

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