mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[PM-20348] Add pending auth request endpoint (#5957)
* Feat(pm-20348): * Add migration scripts for Read Pending Auth Requests by UserId stored procedure and new `view` for pending AuthRequest. * View only returns the most recent pending authRequest, or none at all if the most recent is answered. * Implement stored procedure in AuthRequestRepository for both Dapper and Entity Framework. * Update AuthRequestController to query the new View to get a user's most recent pending auth requests response includes the requesting deviceId. * Doc: * Move summary xml comments to interface. * Added comments for the AuthRequestService. * Test: * Added testing for AuthRequestsController. * Added testing for repositories. * Added integration tests for multiple auth requests but only returning the most recent.
This commit is contained in:
@ -25,9 +25,4 @@
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\Core.Test\Core.Test.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Auth\" />
|
||||
<Folder Include="Auth\Controllers\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
258
test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs
Normal file
258
test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs
Normal file
@ -0,0 +1,258 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Auth.Controllers;
|
||||
using Bit.Api.Auth.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Entities;
|
||||
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;
|
||||
|
||||
namespace Bit.Api.Test.Auth.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(AuthRequestsController))]
|
||||
[SutProviderCustomize]
|
||||
public class AuthRequestsControllerTests
|
||||
{
|
||||
const string _testGlobalSettingsBaseUri = "https://vault.test.dev";
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Get_ReturnsExpectedResult(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns([authRequest]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
var expectedCount = 1;
|
||||
Assert.Equal(result.Data.Count(), expectedCount);
|
||||
Assert.IsType<ListResponseModel<AuthRequestResponseModel>>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetById_ThrowsNotFoundException(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetAuthRequestAsync(authRequest.Id, user.Id)
|
||||
.Returns((AuthRequest)null);
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.Get(authRequest.Id));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetById_ReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetAuthRequestAsync(authRequest.Id, user.Id)
|
||||
.Returns(authRequest);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(authRequest.Id);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPending_ReturnsExpectedResult(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
PendingAuthRequestDetails authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestRepository>()
|
||||
.GetManyPendingAuthRequestByUserId(user.Id)
|
||||
.Returns([authRequest]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPendingAuthRequestsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
var expectedCount = 1;
|
||||
Assert.Equal(result.Data.Count(), expectedCount);
|
||||
Assert.IsType<ListResponseModel<PendingAuthRequestResponseModel>>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetResponseById_ThrowsNotFoundException(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode)
|
||||
.Returns((AuthRequest)null);
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetResponseById_ReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode)
|
||||
.Returns(authRequest);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_AdminApprovalRequest_ThrowsBadRequestException(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
AuthRequestCreateRequestModel authRequest)
|
||||
{
|
||||
// Arrange
|
||||
authRequest.Type = AuthRequestType.AdminApproval;
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.Post(authRequest));
|
||||
|
||||
var expectedMessage = "You must be authenticated to create a request of that type.";
|
||||
Assert.Equal(exception.Message, expectedMessage);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_ReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
AuthRequestCreateRequestModel requestModel,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
|
||||
requestModel.Type = AuthRequestType.AuthenticateAndUnlock;
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.CreateAuthRequestAsync(requestModel)
|
||||
.Returns(authRequest);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Post(requestModel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAdminRequest_ReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
AuthRequestCreateRequestModel requestModel,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
|
||||
requestModel.Type = AuthRequestType.AuthenticateAndUnlock;
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.CreateAuthRequestAsync(requestModel)
|
||||
.Returns(authRequest);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.PostAdminRequest(requestModel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Put_ReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequestUpdateRequestModel requestModel,
|
||||
AuthRequest authRequest)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.UpdateAuthRequestAsync(authRequest.Id, user.Id, requestModel)
|
||||
.Returns(authRequest);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut
|
||||
.Put(authRequest.Id, requestModel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
private void SetBaseServiceUri(SutProvider<AuthRequestsController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri
|
||||
.Vault
|
||||
.Returns(_testGlobalSettingsBaseUri);
|
||||
}
|
||||
}
|
@ -66,10 +66,8 @@ public class AuthRequestRepositoryTests
|
||||
Assert.NotNull(await authRequestRepository.GetByIdAsync(notExpiredAdminApprovalRequest.Id));
|
||||
Assert.NotNull(await authRequestRepository.GetByIdAsync(notExpiredApprovedAdminApprovalRequest.Id));
|
||||
|
||||
// Ensure the repository responds with the amount of items it deleted and it deleted the right amount.
|
||||
// NOTE: On local development this might fail on it's first run because the developer could have expired AuthRequests
|
||||
// on their machine but aren't running the job that would delete them. The second run of this test should succeed.
|
||||
Assert.Equal(4, numberOfDeleted);
|
||||
// Ensure the repository responds with the amount of items it deleted and it deleted the right amount, which could include other auth requests from other tests so we take the minimum known acceptable amount.
|
||||
Assert.True(numberOfDeleted >= 4);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
@ -182,7 +180,157 @@ public class AuthRequestRepositoryTests
|
||||
Assert.Null(uncreatedAuthRequest);
|
||||
}
|
||||
|
||||
private static AuthRequest CreateAuthRequest(Guid userId, AuthRequestType authRequestType, DateTime creationDate, bool? approved = null, DateTime? responseDate = null)
|
||||
/// <summary>
|
||||
/// Test to determine that when no valid authRequest exists in the database the return value is null.
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyPendingAuthRequestByUserId_AuthRequestsInvalid_ReturnsEmptyEnumerable_Success(
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
List<AuthRequest> authRequests = [];
|
||||
|
||||
// A user auth request type that has passed its expiration time, should not be returned.
|
||||
var authRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
CreateExpiredDate(_userRequestExpiration));
|
||||
authRequest.RequestDeviceIdentifier = "auth_request_expired";
|
||||
authRequests.Add(await authRequestRepository.CreateAsync(authRequest));
|
||||
|
||||
// A valid time AuthRequest but for pending we do not fetch admin auth requests
|
||||
authRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AdminApproval,
|
||||
DateTime.UtcNow.AddMinutes(-1));
|
||||
authRequest.RequestDeviceIdentifier = "admin_auth_request";
|
||||
authRequests.Add(await authRequestRepository.CreateAsync(authRequest));
|
||||
|
||||
// A valid time AuthRequest but the request has been approved/rejected, so it should not be returned.
|
||||
authRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-1),
|
||||
false);
|
||||
authRequest.RequestDeviceIdentifier = "approved_auth_request";
|
||||
authRequests.Add(await authRequestRepository.CreateAsync(authRequest));
|
||||
|
||||
var result = await authRequestRepository.GetManyPendingAuthRequestByUserId(user.Id);
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
|
||||
// Verify that there are authRequests associated with the user.
|
||||
Assert.NotEmpty(await authRequestRepository.GetManyByUserIdAsync(user.Id));
|
||||
|
||||
await CleanupTestAsync(authRequests, authRequestRepository);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to determine that when multiple valid authRequest exist for a device only the soonest one is returned.
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyPendingAuthRequestByUserId_MultipleRequestForSingleDevice_ReturnsMostRecent(
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var oneMinuteOldAuthRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-1));
|
||||
oneMinuteOldAuthRequest = await authRequestRepository.CreateAsync(oneMinuteOldAuthRequest);
|
||||
|
||||
var fiveMinuteOldAuthRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-5));
|
||||
fiveMinuteOldAuthRequest = await authRequestRepository.CreateAsync(fiveMinuteOldAuthRequest);
|
||||
|
||||
var tenMinuteOldAuthRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-10));
|
||||
tenMinuteOldAuthRequest = await authRequestRepository.CreateAsync(tenMinuteOldAuthRequest);
|
||||
|
||||
var result = await authRequestRepository.GetManyPendingAuthRequestByUserId(user.Id);
|
||||
Assert.NotNull(result);
|
||||
// since we group by device there should only be a single return since the device Id is the same
|
||||
Assert.Single(result);
|
||||
var resultAuthRequest = result.First();
|
||||
Assert.Equal(oneMinuteOldAuthRequest.Id, resultAuthRequest.Id);
|
||||
|
||||
List<AuthRequest> authRequests = [oneMinuteOldAuthRequest, fiveMinuteOldAuthRequest, tenMinuteOldAuthRequest];
|
||||
|
||||
await CleanupTestAsync(authRequests, authRequestRepository);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to determine that when multiple authRequests exist for a device if the most recent is approved then
|
||||
/// there should be no return.
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyPendingAuthRequestByUserId_MultipleRequestForSingleDevice_MostRecentIsApproved_ReturnsEmpty(
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
// approved auth request
|
||||
var oneMinuteOldAuthRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-1),
|
||||
false);
|
||||
oneMinuteOldAuthRequest = await authRequestRepository.CreateAsync(oneMinuteOldAuthRequest);
|
||||
|
||||
var fiveMinuteOldAuthRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-5));
|
||||
fiveMinuteOldAuthRequest = await authRequestRepository.CreateAsync(fiveMinuteOldAuthRequest);
|
||||
|
||||
var tenMinuteOldAuthRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-10));
|
||||
tenMinuteOldAuthRequest = await authRequestRepository.CreateAsync(tenMinuteOldAuthRequest);
|
||||
|
||||
var result = await authRequestRepository.GetManyPendingAuthRequestByUserId(user.Id);
|
||||
Assert.NotNull(result);
|
||||
// result should be empty since the most recent request was addressed
|
||||
Assert.Empty(result);
|
||||
|
||||
List<AuthRequest> authRequests = [oneMinuteOldAuthRequest, fiveMinuteOldAuthRequest, tenMinuteOldAuthRequest];
|
||||
await CleanupTestAsync(authRequests, authRequestRepository);
|
||||
}
|
||||
|
||||
private static AuthRequest CreateAuthRequest(
|
||||
Guid userId,
|
||||
AuthRequestType authRequestType,
|
||||
DateTime creationDate,
|
||||
bool? approved = null,
|
||||
DateTime? responseDate = null)
|
||||
{
|
||||
return new AuthRequest
|
||||
{
|
||||
@ -203,4 +351,20 @@ public class AuthRequestRepositoryTests
|
||||
var exp = expirationPeriod + TimeSpan.FromMinutes(1);
|
||||
return DateTime.UtcNow.Add(exp.Negate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the test data created by the test methods. This supports the DeleteExpiredAsync Test.
|
||||
/// </summary>
|
||||
/// <param name="authRequests">Created Auth Requests</param>
|
||||
/// <param name="authRequestRepository">repository context for the current test</param>
|
||||
/// <returns>void</returns>
|
||||
private static async Task CleanupTestAsync(
|
||||
IEnumerable<AuthRequest> authRequests,
|
||||
IAuthRequestRepository authRequestRepository)
|
||||
{
|
||||
foreach (var authRequest in authRequests)
|
||||
{
|
||||
await authRequestRepository.DeleteAsync(authRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user