mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 23:52:50 -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:
@ -22,7 +22,6 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using AuthFixtures = Bit.Identity.Test.AutoFixture;
|
||||
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
|
||||
public class BaseRequestValidatorTests
|
||||
@ -82,10 +81,10 @@ public class BaseRequestValidatorTests
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> _Logger.LogInformation
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
* ValidateAsync -> _Logger.LogInformation
|
||||
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
* |-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
@ -112,11 +111,11 @@ public class BaseRequestValidatorTests
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
(self hosted) |-> _logger.LogWarning()
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
* (self hosted) |-> _logger.LogWarning()
|
||||
* |-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResult_ShouldLogWarning(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
@ -140,10 +139,10 @@ public class BaseRequestValidatorTests
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
* |-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
@ -177,134 +176,97 @@ public class BaseRequestValidatorTests
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
|
||||
}
|
||||
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError(
|
||||
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, default)));
|
||||
|
||||
// 1 -> to pass
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "authorization_code";
|
||||
// 2 -> will result to false with no extra configuration
|
||||
// 3 -> set two factor to be false
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
|
||||
// 4 -> set up device validator to fail
|
||||
requestContext.KnownDevice = false;
|
||||
tokenRequest.GrantType = "password";
|
||||
_deviceValidator.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
||||
.Returns(Task.FromResult(false));
|
||||
|
||||
// 5 -> not legacy user
|
||||
_userService.IsLegacyUser(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
Assert.Equal("No device information provided.", errorResponse.Message);
|
||||
await _eventService.Received(1)
|
||||
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, EventType.User_FailedLogIn);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult,
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
|
||||
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(device);
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult,
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
|
||||
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(device);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _eventService.LogUserEventAsync(
|
||||
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
|
||||
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsLegacyUser -> BuildErrorResultAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult(
|
||||
public async Task ValidateAsync_DeviceValidated_ShouldSucceed(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
||||
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
|
||||
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
|
||||
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
|
||||
// 1 -> to pass
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "";
|
||||
// 2 -> will result to false with no extra configuration
|
||||
// 3 -> set two factor to be false
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
|
||||
// 4 -> set up device validator to pass
|
||||
_deviceValidator.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// 5 -> not legacy user
|
||||
_userService.IsLegacyUser(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
}
|
||||
|
||||
// Test grantTypes that require SSO when a user is in an organization that requires it
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredTrue_ShouldSetSsoResult(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(Task.FromResult(true));
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
@ -314,6 +276,85 @@ public class BaseRequestValidatorTests
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
}
|
||||
|
||||
// Test grantTypes where SSO would be required but the user is not in an
|
||||
// organization that requires it
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredFalse_ShouldSucceed(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
|
||||
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(Task.FromResult(false));
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
context.ValidatedTokenRequest.ClientId = "web";
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _eventService.Received(1).LogUserEventAsync(
|
||||
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
|
||||
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
|
||||
}
|
||||
|
||||
// Test the grantTypes where SSO is in progress or not relevant
|
||||
[Theory]
|
||||
[BitAutoData("authorization_code")]
|
||||
[BitAutoData("client_credentials")]
|
||||
public async Task ValidateAsync_GrantTypes_SsoRequiredFalse_ShouldSucceed(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
context.ValidatedTokenRequest.ClientId = "web";
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
await _eventService.Received(1).LogUserEventAsync(
|
||||
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
|
||||
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
}
|
||||
|
||||
/* Logic Path
|
||||
* ValidateAsync -> UserService.IsLegacyUser -> FailAuthForLegacyUserAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
@ -332,6 +373,8 @@ public class BaseRequestValidatorTests
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
@ -339,8 +382,9 @@ public class BaseRequestValidatorTests
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal($"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}"
|
||||
, errorResponse.Message);
|
||||
var expectedMessage = $"Encryption key migration is required. Please log in to the web " +
|
||||
$"vault at {_globalSettings.BaseServiceUri.VaultWithHash}";
|
||||
Assert.Equal(expectedMessage, errorResponse.Message);
|
||||
}
|
||||
|
||||
private BaseRequestValidationContextFake CreateContext(
|
||||
@ -367,4 +411,12 @@ public class BaseRequestValidatorTests
|
||||
Substitute.For<IServiceProvider>(),
|
||||
Substitute.For<ILogger<UserManager<User>>>());
|
||||
}
|
||||
|
||||
private void AddValidDeviceToRequest(ValidatedTokenRequest request)
|
||||
{
|
||||
request.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
||||
request.Raw["DeviceType"] = "Android"; // must be valid device type
|
||||
request.Raw["DeviceName"] = "DeviceName";
|
||||
request.Raw["DevicePushToken"] = "DevicePushToken";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,11 @@ IBaseRequestValidatorTestWrapper
|
||||
Dictionary<string, object> customResponse)
|
||||
{ }
|
||||
|
||||
protected override void SetValidationErrorResult(
|
||||
BaseRequestValidationContextFake context,
|
||||
CustomValidatorRequestContext requestContext)
|
||||
{ }
|
||||
|
||||
protected override Task<bool> ValidateContextAsync(
|
||||
BaseRequestValidationContextFake context,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
|
Reference in New Issue
Block a user