mirror of
https://github.com/bitwarden/server.git
synced 2024-11-30 13:33:24 +01:00
699 lines
27 KiB
C#
699 lines
27 KiB
C#
using Bit.Core.Auth.Entities;
|
|
using Bit.Core.Auth.Enums;
|
|
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 Bit.Test.Common.Helpers;
|
|
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<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest,
|
|
Guid authRequestId,
|
|
Guid userId)
|
|
{
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequestId)
|
|
.Returns(authRequest);
|
|
|
|
var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, userId);
|
|
|
|
Assert.Null(foundAuthRequest);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetAuthRequestAsync_IfSameUser_ReturnsAuthRequest(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest,
|
|
Guid authRequestId)
|
|
{
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.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<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest,
|
|
string accessCode)
|
|
{
|
|
authRequest.CreationDate = DateTime.UtcNow;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, accessCode);
|
|
|
|
Assert.Null(foundAuthRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: AdminApproval AuthRequests should have a longer expiration time by default and non-AdminApproval ones
|
|
/// should expire after 15 minutes by default.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AdminApproval, "-10.00:00:00")]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, "-00:16:00")]
|
|
[BitAutoData(AuthRequestType.Unlock, "-00:16:00")]
|
|
public async Task GetValidatedAuthRequestAsync_IfExpired_ReturnsNull(
|
|
AuthRequestType authRequestType,
|
|
TimeSpan creationTimeBeforeNow,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
authRequest.Type = authRequestType;
|
|
authRequest.CreationDate = DateTime.UtcNow.Add(creationTimeBeforeNow);
|
|
authRequest.Approved = false;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);
|
|
|
|
Assert.Null(foundAuthRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: Once a AdminApproval type has been approved it has a different expiration time based on time
|
|
/// after the response.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData]
|
|
public async Task GetValidatedAuthRequestAsync_AdminApprovalApproved_HasLongerExpiration_ReturnsRequest(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
authRequest.Type = AuthRequestType.AdminApproval;
|
|
authRequest.Approved = true;
|
|
authRequest.ResponseDate = DateTime.UtcNow.Add(TimeSpan.FromHours(-13));
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
var validatedAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);
|
|
|
|
Assert.Null(validatedAuthRequest);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetValidatedAuthRequestAsync_IfValid_ReturnsAuthRequest(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-2);
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth
|
|
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
|
|
|
|
var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);
|
|
|
|
Assert.NotNull(foundAuthRequest);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task CreateAuthRequestAsync_NoUser_ThrowsBadRequest(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequestCreateRequestModel createModel)
|
|
{
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.DeviceType
|
|
.Returns(DeviceType.Android);
|
|
|
|
sutProvider.GetDependency<IUserRepository>()
|
|
.GetByEmailAsync(createModel.Email)
|
|
.Returns((User?)null);
|
|
|
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task CreateAuthRequestAsync_NoKnownDevice_ThrowsBadRequest(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequestCreateRequestModel createModel,
|
|
User user)
|
|
{
|
|
user.Email = createModel.Email;
|
|
|
|
sutProvider.GetDependency<IUserRepository>()
|
|
.GetByEmailAsync(createModel.Email)
|
|
.Returns(user);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.DeviceType
|
|
.Returns(DeviceType.Android);
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth.KnownDevicesOnly
|
|
.Returns(true);
|
|
|
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: Non-AdminApproval requests should be created without a known device if the settings is set to <c>false</c>
|
|
/// Non-AdminApproval ones should also have a push notification sent about them.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
|
|
[BitAutoData(AuthRequestType.Unlock)]
|
|
[BitAutoData(new object?[1] { null })]
|
|
public async Task CreateAuthRequestAsync_CreatesAuthRequest(
|
|
AuthRequestType? authRequestType,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequestCreateRequestModel createModel,
|
|
User user)
|
|
{
|
|
user.Email = createModel.Email;
|
|
createModel.Type = authRequestType;
|
|
|
|
sutProvider.GetDependency<IUserRepository>()
|
|
.GetByEmailAsync(createModel.Email)
|
|
.Returns(user);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.DeviceType
|
|
.Returns(DeviceType.Android);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.IpAddress
|
|
.Returns("1.1.1.1");
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth.KnownDevicesOnly
|
|
.Returns(false);
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.CreateAsync(Arg.Any<AuthRequest>())
|
|
.Returns(c => c.ArgAt<AuthRequest>(0));
|
|
|
|
var createdAuthRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);
|
|
|
|
await sutProvider.GetDependency<IPushNotificationService>()
|
|
.Received()
|
|
.PushAuthRequestAsync(createdAuthRequest);
|
|
|
|
await sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.Received()
|
|
.CreateAsync(createdAuthRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: Since an AllowAnonymous endpoint calls this method we need
|
|
/// to verify that a device was able to be found via ICurrentContext
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
|
|
[BitAutoData(AuthRequestType.Unlock)]
|
|
public async Task CreateAuthRequestAsync_NoDeviceType_ThrowsBadRequest(
|
|
AuthRequestType authRequestType,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequestCreateRequestModel createModel,
|
|
User user)
|
|
{
|
|
user.Email = createModel.Email;
|
|
createModel.Type = authRequestType;
|
|
|
|
sutProvider.GetDependency<IUserRepository>()
|
|
.GetByEmailAsync(createModel.Email)
|
|
.Returns(user);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.DeviceType
|
|
.Returns((DeviceType?)null);
|
|
|
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: If a user happens to exist to more than one organization, we will send the device approval request to
|
|
/// each of them.
|
|
/// </summary>
|
|
[Theory, BitAutoData]
|
|
public async Task CreateAuthRequestAsync_AdminApproval_CreatesForEachOrganization(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequestCreateRequestModel createModel,
|
|
User user,
|
|
OrganizationUser organizationUser1,
|
|
OrganizationUser organizationUser2)
|
|
{
|
|
createModel.Type = AuthRequestType.AdminApproval;
|
|
user.Email = createModel.Email;
|
|
organizationUser1.UserId = user.Id;
|
|
organizationUser2.UserId = user.Id;
|
|
|
|
sutProvider.GetDependency<IUserRepository>()
|
|
.GetByEmailAsync(user.Email)
|
|
.Returns(user);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.DeviceType
|
|
.Returns(DeviceType.ChromeExtension);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>()
|
|
.UserId
|
|
.Returns(user.Id);
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth.KnownDevicesOnly
|
|
.Returns(false);
|
|
|
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
.GetManyByUserAsync(user.Id)
|
|
.Returns(new List<OrganizationUser>
|
|
{
|
|
organizationUser1,
|
|
organizationUser2,
|
|
});
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.CreateAsync(Arg.Any<AuthRequest>())
|
|
.Returns(c => c.ArgAt<AuthRequest>(0));
|
|
|
|
var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);
|
|
|
|
Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId);
|
|
|
|
await sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.Received(1)
|
|
.CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser1.OrganizationId));
|
|
|
|
await sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.Received(1)
|
|
.CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser2.OrganizationId));
|
|
|
|
await sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.Received(2)
|
|
.CreateAsync(Arg.Any<AuthRequest>());
|
|
|
|
await sutProvider.GetDependency<IEventService>()
|
|
.Received(1)
|
|
.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: When an <see cref="AuthRequest"> is approved we want to update it in the database so it cannot have
|
|
/// it's status changed again and we want to push a notification to let the user know of the approval.
|
|
/// In the case of the AdminApproval we also want to log an event.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AdminApproval, "7b055ea1-38be-42d0-b2e4-becb2340f8df")]
|
|
[BitAutoData(AuthRequestType.Unlock, null)]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]
|
|
public async Task UpdateAuthRequestAsync_ValidResponse_SendsResponse(
|
|
AuthRequestType authRequestType,
|
|
Guid? organizationId,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
|
|
authRequest.Approved = null;
|
|
authRequest.OrganizationId = organizationId;
|
|
authRequest.Type = authRequestType;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
var device = new Device
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Identifier = "test_identifier",
|
|
};
|
|
|
|
sutProvider.GetDependency<IDeviceRepository>()
|
|
.GetByIdentifierAsync(device.Identifier, authRequest.UserId)
|
|
.Returns(device);
|
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
|
|
.Returns(new OrganizationUser
|
|
{
|
|
UserId = authRequest.UserId,
|
|
OrganizationId = organizationId.GetValueOrDefault(),
|
|
});
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth
|
|
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
|
|
|
|
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);
|
|
|
|
// On approval, the response date should be set to current date
|
|
Assert.NotNull(udpatedAuthRequest.ResponseDate);
|
|
AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);
|
|
|
|
await sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.Received(1)
|
|
.ReplaceAsync(udpatedAuthRequest);
|
|
|
|
await sutProvider.GetDependency<IPushNotificationService>()
|
|
.Received(1)
|
|
.PushAuthRequestResponseAsync(udpatedAuthRequest);
|
|
|
|
var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;
|
|
await sutProvider.GetDependency<IEventService>()
|
|
.Received(expectedNumberOfCalls)
|
|
.LogOrganizationUserEventAsync(
|
|
Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),
|
|
EventType.OrganizationUser_ApprovedAuthRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: When an <see cref="AuthRequest"> is rejected we want to update it in the database so it cannot have
|
|
/// it's status changed again but we do not want to send a push notification to the original device
|
|
/// so as to not leak that it was rejected. In the case of an AdminApproval type we do want to log an event though
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AdminApproval, "7b055ea1-38be-42d0-b2e4-becb2340f8df")]
|
|
[BitAutoData(AuthRequestType.Unlock, null)]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]
|
|
public async Task UpdateAuthRequestAsync_ResponseNotApproved_DoesNotLeakRejection(
|
|
AuthRequestType authRequestType,
|
|
Guid? organizationId,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
// Give it a recent creation time which is valid for all types of AuthRequests
|
|
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
|
|
authRequest.Type = authRequestType;
|
|
// Has not been decided already
|
|
authRequest.Approved = null;
|
|
authRequest.OrganizationId = organizationId;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
// Setup a device for all requests even though it will not be called for verification in a AdminApproval
|
|
var device = new Device
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Identifier = "test_identifier",
|
|
};
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth
|
|
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
|
|
|
|
sutProvider.GetDependency<IDeviceRepository>()
|
|
.GetByIdentifierAsync(device.Identifier, authRequest.UserId)
|
|
.Returns(device);
|
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
|
|
.Returns(new OrganizationUser
|
|
{
|
|
UserId = authRequest.UserId,
|
|
OrganizationId = organizationId.GetValueOrDefault(),
|
|
});
|
|
|
|
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);
|
|
Assert.False(udpatedAuthRequest.Approved);
|
|
Assert.NotNull(udpatedAuthRequest.ResponseDate);
|
|
AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);
|
|
|
|
await sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.Received()
|
|
.ReplaceAsync(udpatedAuthRequest);
|
|
|
|
await sutProvider.GetDependency<IPushNotificationService>()
|
|
.DidNotReceiveWithAnyArgs()
|
|
.PushAuthRequestResponseAsync(udpatedAuthRequest);
|
|
|
|
var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;
|
|
|
|
await sutProvider.GetDependency<IEventService>()
|
|
.Received(expectedNumberOfCalls)
|
|
.LogOrganizationUserEventAsync(
|
|
Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),
|
|
EventType.OrganizationUser_RejectedAuthRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: A bad actor is able to get ahold of the request id of a valid <see cref="AuthRequest" />
|
|
/// and tries to approve it from their own Bitwarden account. We need to validate that the currently signed in user
|
|
/// is the same user that originally created the request and we want to pretend it does not exist at all by throwing
|
|
/// NotFoundException.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
|
|
[BitAutoData(AuthRequestType.Unlock)]
|
|
public async Task UpdateAuthRequestAsync_InvalidUser_ThrowsNotFound(
|
|
AuthRequestType authRequestType,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest,
|
|
Guid authenticatedUserId)
|
|
{
|
|
// Give it a recent creation date so that it is valid
|
|
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
|
|
// The request hasn't been Approved/Disapproved already
|
|
authRequest.Approved = null;
|
|
// Has an type that needs the UserId property validated
|
|
authRequest.Type = authRequestType;
|
|
|
|
// Auth request should not be null
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.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<NotFoundException>(
|
|
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authenticatedUserId, updateModel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: A user created this auth request and does not approve/reject the request
|
|
/// for 16 minutes, which is past the default expiration time. This auth request
|
|
/// will be purged from the database soon but might exist for some amount of time after it's expiration
|
|
/// this method should throw a NotFoundException since it theoretically should not exist, this
|
|
/// could be a user finally clicking Approve after the request sitting on their phone for a while.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, "-00:16:00")]
|
|
[BitAutoData(AuthRequestType.Unlock, "-00:16:00")]
|
|
[BitAutoData(AuthRequestType.AdminApproval, "-8.00:00:00")]
|
|
public async Task UpdateAuthRequestAsync_OldAuthRequest_ThrowsNotFound(
|
|
AuthRequestType authRequestType,
|
|
TimeSpan timeBeforeCreation,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
// AuthRequest's have a default valid lifetime of only 15 minutes, make it older than that
|
|
authRequest.CreationDate = DateTime.UtcNow.Add(timeBeforeCreation);
|
|
// Make it so that the user has not made a decision on this request
|
|
authRequest.Approved = null;
|
|
// Make it one of the types that doesn't have longer expiration i.e AdminApproval
|
|
authRequest.Type = authRequestType;
|
|
|
|
// The item should still exist in the database
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
// Represents the user finally clicking approve.
|
|
var updateModel = new AuthRequestUpdateRequestModel
|
|
{
|
|
Key = "test_key",
|
|
DeviceIdentifier = "test_identifier",
|
|
RequestApproved = true,
|
|
MasterPasswordHash = "my_hash",
|
|
};
|
|
|
|
await Assert.ThrowsAsync<NotFoundException>(
|
|
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: non-AdminApproval types need to validate that the device used to respond to the
|
|
/// request is a known device to the authenticated user.
|
|
/// </summary>
|
|
[Theory]
|
|
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
|
|
[BitAutoData(AuthRequestType.Unlock)]
|
|
public async Task UpdateAuthRequestAsync_InvalidDeviceIdentifier_ThrowsBadRequest(
|
|
AuthRequestType authRequestType,
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
|
|
authRequest.Approved = null;
|
|
authRequest.Type = authRequestType;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
sutProvider.GetDependency<IDeviceRepository>()
|
|
.GetByIdentifierAsync("invalid_identifier", authRequest.UserId)
|
|
.Returns((Device?)null);
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth
|
|
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
|
|
|
|
var updateModel = new AuthRequestUpdateRequestModel
|
|
{
|
|
Key = "test_key",
|
|
DeviceIdentifier = "invalid_identifier",
|
|
RequestApproved = true,
|
|
MasterPasswordHash = "my_hash",
|
|
};
|
|
|
|
await Assert.ThrowsAsync<BadRequestException>(
|
|
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: Once the destiny of an AuthRequest has been decided, it should be considered immutable
|
|
/// and new update request should be blocked.
|
|
/// </summary>
|
|
[Theory, BitAutoData]
|
|
public async Task UpdateAuthRequestAsync_AlreadyApprovedOrRejected_ThrowsDuplicateAuthRequestException(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest)
|
|
{
|
|
authRequest.Approved = true;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
var updateModel = new AuthRequestUpdateRequestModel
|
|
{
|
|
Key = "test_key",
|
|
DeviceIdentifier = "test_identifier",
|
|
RequestApproved = true,
|
|
MasterPasswordHash = "my_hash",
|
|
};
|
|
|
|
await Assert.ThrowsAsync<DuplicateAuthRequestException>(
|
|
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Story: An admin approves a request for one of their org users. For auditing purposes we need to
|
|
/// log an event that correlates the action for who the request was approved for. On approval we also need to
|
|
/// push the notification to the user.
|
|
/// </summary>
|
|
[Theory, BitAutoData]
|
|
public async Task UpdateAuthRequestAsync_AdminApproved_LogsEvent(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
AuthRequest authRequest,
|
|
OrganizationUser organizationUser)
|
|
{
|
|
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
|
|
authRequest.Type = AuthRequestType.AdminApproval;
|
|
authRequest.OrganizationId = organizationUser.OrganizationId;
|
|
authRequest.Approved = null;
|
|
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequest.Id)
|
|
.Returns(authRequest);
|
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
.GetByOrganizationAsync(authRequest.OrganizationId!.Value, authRequest.UserId)
|
|
.Returns(organizationUser);
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.PasswordlessAuth
|
|
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
|
|
|
|
var updateModel = new AuthRequestUpdateRequestModel
|
|
{
|
|
Key = "test_key",
|
|
RequestApproved = true,
|
|
MasterPasswordHash = "my_hash",
|
|
};
|
|
|
|
var updatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);
|
|
|
|
Assert.Equal("my_hash", updatedAuthRequest.MasterPasswordHash);
|
|
Assert.Equal("test_key", updatedAuthRequest.Key);
|
|
Assert.True(updatedAuthRequest.Approved);
|
|
Assert.NotNull(updatedAuthRequest.ResponseDate);
|
|
AssertHelper.AssertRecent(updatedAuthRequest.ResponseDate!.Value);
|
|
|
|
await sutProvider.GetDependency<IEventService>()
|
|
.Received(1)
|
|
.LogOrganizationUserEventAsync(
|
|
Arg.Is(organizationUser), Arg.Is(EventType.OrganizationUser_ApprovedAuthRequest));
|
|
|
|
await sutProvider.GetDependency<IPushNotificationService>()
|
|
.Received(1)
|
|
.PushAuthRequestResponseAsync(authRequest);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task UpdateAuthRequestAsync_BadId_ThrowsNotFound(
|
|
SutProvider<AuthRequestService> sutProvider,
|
|
Guid authRequestId)
|
|
{
|
|
sutProvider.GetDependency<IAuthRequestRepository>()
|
|
.GetByIdAsync(authRequestId)
|
|
.Returns((AuthRequest?)null);
|
|
|
|
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAuthRequestAsync(
|
|
authRequestId, Guid.NewGuid(), new AuthRequestUpdateRequestModel()));
|
|
}
|
|
}
|