using Bit.Core.Auth.Entities; using Bit.Core.Auth.Exceptions; using Bit.Core.Auth.Models.Api.Request.AuthRequest; using Bit.Core.Auth.Services.Implementations; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; #nullable enable namespace Bit.Core.Test.Auth.Services; [SutProviderCustomize] public class AuthRequestServiceTests { [Theory, BitAutoData] public async Task GetAuthRequestAsync_IfDifferentUser_ReturnsNull( SutProvider sutProvider, AuthRequest authRequest, Guid authRequestId, Guid userId) { sutProvider.GetDependency() .GetByIdAsync(authRequestId) .Returns(authRequest); var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, userId); Assert.Null(foundAuthRequest); } [Theory, BitAutoData] public async Task GetAuthRequestAsync_IfSameUser_ReturnsAuthRequest( SutProvider sutProvider, AuthRequest authRequest, Guid authRequestId) { sutProvider.GetDependency() .GetByIdAsync(authRequestId) .Returns(authRequest); var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, authRequest.UserId); Assert.NotNull(foundAuthRequest); } [Theory, BitAutoData] public async Task GetValidatedAuthRequestAsync_IfCodeNotValid_ReturnsNull( SutProvider sutProvider, AuthRequest authRequest, string accessCode) { authRequest.CreationDate = DateTime.UtcNow; sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, accessCode); Assert.Null(foundAuthRequest); } [Theory, BitAutoData] public async Task GetValidatedAuthRequestAsync_IfExpired_ReturnsNull( SutProvider sutProvider, AuthRequest authRequest) { authRequest.CreationDate = DateTime.UtcNow.AddHours(-1); sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode); Assert.Null(foundAuthRequest); } [Theory, BitAutoData] public async Task GetValidatedAuthRequestAsync_IfValid_ReturnsAuthRequest( SutProvider sutProvider, AuthRequest authRequest) { authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-2); sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode); Assert.NotNull(foundAuthRequest); } [Theory, BitAutoData] public async Task CreateAuthRequestAsync_NoUser_ThrowsNotFound( SutProvider sutProvider, AuthRequestCreateRequestModel createModel) { sutProvider.GetDependency() .GetByEmailAsync(createModel.Email) .Returns((User?)null); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAuthRequestAsync(createModel)); } [Theory, BitAutoData] public async Task CreateAuthRequestAsync_NoKnownDevice_ThrowsBadRequest( SutProvider sutProvider, AuthRequestCreateRequestModel createModel, User user) { user.Email = createModel.Email; sutProvider.GetDependency() .GetByEmailAsync(createModel.Email) .Returns(user); sutProvider.GetDependency() .DeviceType .Returns(DeviceType.Android); sutProvider.GetDependency() .PasswordlessAuth.KnownDevicesOnly .Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAuthRequestAsync(createModel)); } [Theory, BitAutoData] public async Task CreateAuthRequestAsync_CreatesAuthRequest( SutProvider sutProvider, AuthRequestCreateRequestModel createModel, User user) { user.Email = createModel.Email; sutProvider.GetDependency() .GetByEmailAsync(createModel.Email) .Returns(user); sutProvider.GetDependency() .DeviceType .Returns(DeviceType.Android); sutProvider.GetDependency() .PasswordlessAuth.KnownDevicesOnly .Returns(false); await sutProvider.Sut.CreateAuthRequestAsync(createModel); await sutProvider.GetDependency() .Received() .PushAuthRequestAsync(Arg.Any()); await sutProvider.GetDependency() .Received() .CreateAsync(Arg.Any()); } [Theory, BitAutoData] public async Task CreateAuthRequestAsync_NoDeviceType_ThrowsBadRequest( SutProvider sutProvider, AuthRequestCreateRequestModel createModel, User user) { user.Email = createModel.Email; sutProvider.GetDependency() .GetByEmailAsync(createModel.Email) .Returns(user); sutProvider.GetDependency() .DeviceType .Returns((DeviceType?)null); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAuthRequestAsync(createModel)); } [Theory, BitAutoData] public async Task UpdateAuthRequestAsync_ValidResponse_SendsResponse( SutProvider sutProvider, AuthRequest authRequest) { authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10); authRequest.Approved = null; sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var device = new Device { Id = Guid.NewGuid(), Identifier = "test_identifier", }; sutProvider.GetDependency() .GetByIdentifierAsync(device.Identifier, authRequest.UserId) .Returns(device); var updateModel = new AuthRequestUpdateRequestModel { Key = "test_key", DeviceIdentifier = "test_identifier", RequestApproved = true, MasterPasswordHash = "my_hash", }; var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel); Assert.Equal("my_hash", udpatedAuthRequest.MasterPasswordHash); await sutProvider.GetDependency() .Received() .ReplaceAsync(udpatedAuthRequest); await sutProvider.GetDependency() .Received() .PushAuthRequestResponseAsync(udpatedAuthRequest); } [Theory, BitAutoData] public async Task UpdateAuthRequestAsync_ResponseNotApproved_DoesNotLeakRejection( SutProvider sutProvider, AuthRequest authRequest) { authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10); authRequest.Approved = null; sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var device = new Device { Id = Guid.NewGuid(), Identifier = "test_identifier", }; sutProvider.GetDependency() .GetByIdentifierAsync(device.Identifier, authRequest.UserId) .Returns(device); var updateModel = new AuthRequestUpdateRequestModel { Key = "test_key", DeviceIdentifier = "test_identifier", RequestApproved = false, MasterPasswordHash = "my_hash", }; var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel); Assert.Equal(udpatedAuthRequest.MasterPasswordHash, authRequest.MasterPasswordHash); await sutProvider.GetDependency() .Received() .ReplaceAsync(udpatedAuthRequest); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .PushAuthRequestResponseAsync(udpatedAuthRequest); } [Theory, BitAutoData] public async Task UpdateAuthRequestAsync_InvalidUser_ThrowsNotFound( SutProvider sutProvider, AuthRequest authRequest, Guid userId) { // Give it a recent creation date so that it is valid authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10); authRequest.Approved = false; // Auth request should not be null sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var updateModel = new AuthRequestUpdateRequestModel { Key = "test_key", DeviceIdentifier = "test_identifier", RequestApproved = true, MasterPasswordHash = "my_hash", }; // Give it a randomly generated userId such that it won't be valid for the AuthRequest await Assert.ThrowsAsync( async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, userId, updateModel)); } [Theory, BitAutoData] public async Task UpdateAuthRequestAsync_OldAuthRequest_ThrowsNotFound( SutProvider sutProvider, AuthRequest authRequest) { // AuthRequest's have a valid lifetime of only 15 minutes, make it older than that authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-16); authRequest.Approved = false; // Auth request should not be null sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var updateModel = new AuthRequestUpdateRequestModel { Key = "test_key", DeviceIdentifier = "test_identifier", RequestApproved = true, MasterPasswordHash = "my_hash", }; // Give it a randomly generated userId such that it won't be valid for the AuthRequest await Assert.ThrowsAsync( async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel)); } [Theory, BitAutoData] public async Task UpdateAuthRequestAsync_InvalidDeviceIdentifier_ThrowsBadRequest( SutProvider sutProvider, AuthRequest authRequest) { authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10); authRequest.Approved = null; sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); sutProvider.GetDependency() .GetByIdentifierAsync(Arg.Any(), authRequest.UserId) .Returns((Device?)null); var updateModel = new AuthRequestUpdateRequestModel { Key = "test_key", DeviceIdentifier = "invalid_identifier", RequestApproved = true, MasterPasswordHash = "my_hash", }; await Assert.ThrowsAsync( async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel)); } [Theory, BitAutoData] public async Task UpdateAuthRequestAsync_AlreadyApprovedOrRejected_ThrowsDuplicateAuthRequestException( SutProvider sutProvider, AuthRequest authRequest) { // Set CreationDate to a valid recent value and Approved to a non-null value authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10); authRequest.Approved = true; sutProvider.GetDependency() .GetByIdAsync(authRequest.Id) .Returns(authRequest); var device = new Device { Id = Guid.NewGuid(), Identifier = "test_identifier", }; sutProvider.GetDependency() .GetByIdentifierAsync(device.Identifier, authRequest.UserId) .Returns(device); var updateModel = new AuthRequestUpdateRequestModel { Key = "test_key", DeviceIdentifier = "test_identifier", RequestApproved = true, MasterPasswordHash = "my_hash", }; await Assert.ThrowsAsync( async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel)); } }