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; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; using AuthFixtures = Bit.Identity.Test.AutoFixture; namespace Bit.Identity.Test.IdentityServer; public class DeviceValidatorTests { private readonly IDeviceService _deviceService; private readonly IDeviceRepository _deviceRepository; private readonly GlobalSettings _globalSettings; private readonly IMailService _mailService; private readonly ICurrentContext _currentContext; private readonly IUserService _userService; private readonly IDistributedCache _distributedCache; private readonly Logger _logger; private readonly IFeatureService _featureService; private readonly DeviceValidator _sut; public DeviceValidatorTests() { _deviceService = Substitute.For(); _deviceRepository = Substitute.For(); _globalSettings = new GlobalSettings(); _mailService = Substitute.For(); _currentContext = Substitute.For(); _userService = Substitute.For(); _distributedCache = Substitute.For(); _logger = new Logger(Substitute.For()); _featureService = Substitute.For(); _sut = new DeviceValidator( _deviceService, _deviceRepository, _globalSettings, _mailService, _currentContext, _userService, _distributedCache, _logger, _featureService); } [Theory, BitAutoData] public async void GetKnownDeviceAsync_UserNull_ReturnsFalse( Device device) { // Arrange // AutoData arranges // Act var result = await _sut.GetKnownDeviceAsync(null, device); // Assert Assert.Null(result); } [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.GetKnownDeviceAsync(user, null); // Assert Assert.Null(result); } [Theory, BitAutoData] public async void GetKnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse( User user, Device device) { // Arrange _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) .Returns(null as Device); // Act var result = await _sut.GetKnownDeviceAsync(user, device); // Assert Assert.Null(result); } [Theory, BitAutoData] public async void GetKnownDeviceAsync_UserAndDeviceValid_ReturnsTrue( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, User user, Device device) { // Arrange AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) .Returns(device); // Act 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()); 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(), Arg.Any()) .Returns(device); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert await _deviceService.Received(0).SaveAsync(Arg.Any()); 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(), Arg.Any()) .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()); Assert.True(result); Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); Assert.Equal(context.Device, databaseDevice); } [Theory, BitAutoData] public async void ValidateRequestDeviceAsync_ExistingUserNewDeviceLogin_SendNewDeviceLoginEmail_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); // 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(), Arg.Any(), Arg.Any(), Arg.Any()); Assert.True(result); } [Theory, BitAutoData] public async void ValidateRequestDeviceAsync_NewUserNewDeviceLogin_DoesNotSendNewDeviceLoginEmail_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); // 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(), Arg.Any(), Arg.Any(), Arg.Any()); Assert.True(result); } [Theory, BitAutoData] public async void ValidateRequestDeviceAsynce_DisableNewDeviceLoginEmailTrue_DoesNotSendNewDeviceEmail_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); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert await _deviceService.Received(1).SaveAsync(context.Device); await _mailService.Received(0).SendNewDeviceLoggedInEmail( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); 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()); 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_VerifyDevicesFalse_ReturnsSuccess( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User.VerifyDevices = false; // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert await _userService.Received(0).SendOTPAsync(context.User); await _deviceService.Received(1).SaveAsync(Arg.Any()); 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_UserHasCacheValue_ReturnsSuccess( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert await _userService.Received(0).SendOTPAsync(context.User); await _deviceService.Received(1).SaveAsync(Arg.Any()); 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_NewDeviceOtpValid_ReturnsSuccess( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); 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; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); 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()); await _deviceService.Received(0).SaveAsync(Arg.Any()); 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; _distributedCache.GetAsync(Arg.Any()).Returns([1]); _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert await _userService.Received(0).VerifyOTPAsync(Arg.Any(), Arg.Any()); await _userService.Received(0).SendOTPAsync(Arg.Any()); 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()]); _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert await _userService.Received(1).SendNewDeviceVerificationEmailAsync(context.User); await _deviceService.Received(0).SaveAsync(Arg.Any()); 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 static 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"; } /// /// Configures the request context to facilitate testing the HandleNewDeviceVerificationAsync method. /// /// test context /// test request private static void ArrangeForHandleNewDeviceVerificationTest( CustomValidatorRequestContext context, ValidatedTokenRequest request) { context.KnownDevice = false; request.GrantType = "password"; context.TwoFactorRequired = false; context.SsoRequired = false; } }