1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

[PM-1807] Add Auth Request Service (#2900)

* Refactor AuthRequest Logic into Service

* Add Tests & Run Formatting

* Register Service

* Add Tests From PR Feedback

Co-authored-by: Jared Snider <jsnider@bitwarden.com>

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
This commit is contained in:
Justin Baur 2023-05-09 12:39:33 -04:00 committed by GitHub
parent f9038472ce
commit 5a850f48e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 590 additions and 104 deletions

View File

@ -1,14 +1,11 @@
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Response;
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Exceptions;
using Bit.Core.Context;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -18,30 +15,21 @@ namespace Bit.Api.Auth.Controllers;
[Authorize("Application")]
public class AuthRequestsController : Controller
{
private readonly IUserRepository _userRepository;
private readonly IDeviceRepository _deviceRepository;
private readonly IUserService _userService;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly ICurrentContext _currentContext;
private readonly IPushNotificationService _pushNotificationService;
private readonly IGlobalSettings _globalSettings;
private readonly IAuthRequestService _authRequestService;
public AuthRequestsController(
IUserRepository userRepository,
IDeviceRepository deviceRepository,
IUserService userService,
IAuthRequestRepository authRequestRepository,
ICurrentContext currentContext,
IPushNotificationService pushNotificationService,
IGlobalSettings globalSettings)
IGlobalSettings globalSettings,
IAuthRequestService authRequestService)
{
_userRepository = userRepository;
_deviceRepository = deviceRepository;
_userService = userService;
_authRequestRepository = authRequestRepository;
_currentContext = currentContext;
_pushNotificationService = pushNotificationService;
_globalSettings = globalSettings;
_authRequestService = authRequestService;
}
[HttpGet("")]
@ -54,11 +42,12 @@ public class AuthRequestsController : Controller
}
[HttpGet("{id}")]
public async Task<AuthRequestResponseModel> Get(string id)
public async Task<AuthRequestResponseModel> Get(Guid id)
{
var userId = _userService.GetProperUserId(User).Value;
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
if (authRequest == null || authRequest.UserId != userId)
var authRequest = await _authRequestService.GetAuthRequestAsync(id, userId);
if (authRequest == null)
{
throw new NotFoundException();
}
@ -68,10 +57,11 @@ public class AuthRequestsController : Controller
[HttpGet("{id}/response")]
[AllowAnonymous]
public async Task<AuthRequestResponseModel> GetResponse(string id, [FromQuery] string code)
public async Task<AuthRequestResponseModel> GetResponse(Guid id, [FromQuery] string code)
{
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code) || authRequest.GetExpirationDate() < DateTime.UtcNow)
var authRequest = await _authRequestService.GetValidatedAuthRequestAsync(id, code);
if (authRequest == null)
{
throw new NotFoundException();
}
@ -83,80 +73,16 @@ public class AuthRequestsController : Controller
[AllowAnonymous]
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
{
var user = await _userRepository.GetByEmailAsync(model.Email);
if (user == null)
{
throw new NotFoundException();
}
if (!_currentContext.DeviceType.HasValue)
{
throw new BadRequestException("Device type not provided.");
}
if (_globalSettings.PasswordlessAuth.KnownDevicesOnly)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
if (devices == null || !devices.Any(d => d.Identifier == model.DeviceIdentifier))
{
throw new BadRequestException("Login with device is only available on devices that have been previously logged in.");
}
}
var authRequest = new AuthRequest
{
RequestDeviceIdentifier = model.DeviceIdentifier,
RequestDeviceType = _currentContext.DeviceType.Value,
RequestIpAddress = _currentContext.IpAddress,
AccessCode = model.AccessCode,
PublicKey = model.PublicKey,
UserId = user.Id,
Type = model.Type.Value
};
authRequest = await _authRequestRepository.CreateAsync(authRequest);
await _pushNotificationService.PushAuthRequestAsync(authRequest);
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
return r;
}
[HttpPut("{id}")]
public async Task<AuthRequestResponseModel> Put(string id, [FromBody] AuthRequestUpdateRequestModel model)
public async Task<AuthRequestResponseModel> Put(Guid id, [FromBody] AuthRequestUpdateRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
if (authRequest == null || authRequest.UserId != userId || authRequest.GetExpirationDate() < DateTime.UtcNow)
{
throw new NotFoundException();
}
if (authRequest.Approved is not null)
{
throw new DuplicateAuthRequestException();
}
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, userId);
if (device == null)
{
throw new BadRequestException("Invalid device.");
}
authRequest.ResponseDeviceId = device.Id;
authRequest.ResponseDate = DateTime.UtcNow;
authRequest.Approved = model.RequestApproved;
if (model.RequestApproved)
{
authRequest.Key = model.Key;
authRequest.MasterPasswordHash = model.MasterPasswordHash;
}
await _authRequestRepository.ReplaceAsync(authRequest);
// We only want to send an approval notification if the request is approved (or null),
// to not leak that it was denied to the originating client if it was originated by a malicious actor.
if (authRequest.Approved ?? true)
{
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
}
var authRequest = await _authRequestService.UpdateAuthRequestAsync(id, userId, model);
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
}
}

View File

@ -1,8 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Auth.Enums;
using Newtonsoft.Json;
namespace Bit.Api.Auth.Models.Request;
namespace Bit.Core.Auth.Models.Api.Request.AuthRequest;
public class AuthRequestCreateRequestModel
{
@ -18,13 +17,3 @@ public class AuthRequestCreateRequestModel
[Required]
public AuthRequestType? Type { get; set; }
}
public class AuthRequestUpdateRequestModel
{
public string Key { get; set; }
public string MasterPasswordHash { get; set; }
[Required]
public string DeviceIdentifier { get; set; }
[Required]
public bool RequestApproved { get; set; }
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Auth.Models.Api.Request.AuthRequest;
public class AuthRequestUpdateRequestModel
{
public string Key { get; set; }
public string MasterPasswordHash { get; set; }
[Required]
public string DeviceIdentifier { get; set; }
[Required]
public bool RequestApproved { get; set; }
}

View File

@ -0,0 +1,14 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
#nullable enable
namespace Bit.Core.Auth.Services;
public interface IAuthRequestService
{
Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId);
Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code);
Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model);
Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model);
}

View File

@ -0,0 +1,149 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Exceptions;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Auth.Services.Implementations;
public class AuthRequestService : IAuthRequestService
{
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IUserRepository _userRepository;
private readonly IGlobalSettings _globalSettings;
private readonly IDeviceRepository _deviceRepository;
private readonly ICurrentContext _currentContext;
private readonly IPushNotificationService _pushNotificationService;
public AuthRequestService(
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository,
IGlobalSettings globalSettings,
IDeviceRepository deviceRepository,
ICurrentContext currentContext,
IPushNotificationService pushNotificationService)
{
_authRequestRepository = authRequestRepository;
_userRepository = userRepository;
_globalSettings = globalSettings;
_deviceRepository = deviceRepository;
_currentContext = currentContext;
_pushNotificationService = pushNotificationService;
}
public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
{
var authRequest = await _authRequestRepository.GetByIdAsync(id);
if (authRequest == null || authRequest.UserId != userId)
{
return null;
}
return authRequest;
}
public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code)
{
var authRequest = await _authRequestRepository.GetByIdAsync(id);
if (authRequest == null ||
!CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code) ||
authRequest.GetExpirationDate() < DateTime.UtcNow)
{
return null;
}
return authRequest;
}
/// <summary>
/// Validates and Creates an <see cref="AuthRequest" /> in the database, as well as pushes it through notifications services
/// </summary>
/// <remarks>
/// This method can only be called inside of an HTTP call because of it's reliance on <see cref="ICurrentContext" />
/// </remarks>
public async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model)
{
var user = await _userRepository.GetByEmailAsync(model.Email);
if (user == null)
{
throw new NotFoundException();
}
if (!_currentContext.DeviceType.HasValue)
{
throw new BadRequestException("Device type not provided.");
}
if (_globalSettings.PasswordlessAuth.KnownDevicesOnly)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
if (devices == null || !devices.Any(d => d.Identifier == model.DeviceIdentifier))
{
throw new BadRequestException(
"Login with device is only available on devices that have been previously logged in.");
}
}
var authRequest = new AuthRequest
{
RequestDeviceIdentifier = model.DeviceIdentifier,
RequestDeviceType = _currentContext.DeviceType.Value,
RequestIpAddress = _currentContext.IpAddress,
AccessCode = model.AccessCode,
PublicKey = model.PublicKey,
UserId = user.Id,
Type = model.Type.GetValueOrDefault(),
};
authRequest = await _authRequestRepository.CreateAsync(authRequest);
await _pushNotificationService.PushAuthRequestAsync(authRequest);
return authRequest;
}
public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model)
{
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
if (authRequest == null || authRequest.UserId != userId || authRequest.GetExpirationDate() < DateTime.UtcNow)
{
throw new NotFoundException();
}
if (authRequest.Approved is not null)
{
throw new DuplicateAuthRequestException();
}
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, userId);
if (device == null)
{
throw new BadRequestException("Invalid device.");
}
authRequest.ResponseDeviceId = device.Id;
authRequest.ResponseDate = DateTime.UtcNow;
authRequest.Approved = model.RequestApproved;
if (model.RequestApproved)
{
authRequest.Key = model.Key;
authRequest.MasterPasswordHash = model.MasterPasswordHash;
}
await _authRequestRepository.ReplaceAsync(authRequest);
// We only want to send an approval notification if the request is approved (or null),
// to not leak that it was denied to the originating client if it was originated by a malicious actor.
if (authRequest.Approved ?? true)
{
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
}
return authRequest;
}
}

View File

@ -8,6 +8,7 @@ using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.LoginFeatures;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.Services.Implementations;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.HostedServices;
@ -130,6 +131,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IDeviceService, DeviceService>();
services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<IAuthRequestService, AuthRequestService>();
services.AddScoped<ISendService, SendService>();
services.AddLoginServices();
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();

View File

@ -0,0 +1,393 @@
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<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);
}
[Theory, BitAutoData]
public async Task GetValidatedAuthRequestAsync_IfExpired_ReturnsNull(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddHours(-1);
sutProvider.GetDependency<IAuthRequestRepository>()
.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<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-2);
sutProvider.GetDependency<IAuthRequestRepository>()
.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<AuthRequestService> sutProvider,
AuthRequestCreateRequestModel createModel)
{
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(createModel.Email)
.Returns((User?)null);
await Assert.ThrowsAsync<NotFoundException>(() => 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));
}
[Theory, BitAutoData]
public async Task CreateAuthRequestAsync_CreatesAuthRequest(
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(false);
await sutProvider.Sut.CreateAuthRequestAsync(createModel);
await sutProvider.GetDependency<IPushNotificationService>()
.Received()
.PushAuthRequestAsync(Arg.Any<AuthRequest>());
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received()
.CreateAsync(Arg.Any<AuthRequest>());
}
[Theory, BitAutoData]
public async Task CreateAuthRequestAsync_NoDeviceType_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?)null);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_ValidResponse_SendsResponse(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = null;
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);
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<IAuthRequestRepository>()
.Received()
.ReplaceAsync(udpatedAuthRequest);
await sutProvider.GetDependency<IPushNotificationService>()
.Received()
.PushAuthRequestResponseAsync(udpatedAuthRequest);
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_ResponseNotApproved_DoesNotLeakRejection(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = null;
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);
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<IAuthRequestRepository>()
.Received()
.ReplaceAsync(udpatedAuthRequest);
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushAuthRequestResponseAsync(udpatedAuthRequest);
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_InvalidUser_ThrowsNotFound(
SutProvider<AuthRequestService> 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<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, userId, updateModel));
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_OldAuthRequest_ThrowsNotFound(
SutProvider<AuthRequestService> 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<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, authRequest.UserId, updateModel));
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_InvalidDeviceIdentifier_ThrowsBadRequest(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = null;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
sutProvider.GetDependency<IDeviceRepository>()
.GetByIdentifierAsync(Arg.Any<string>(), authRequest.UserId)
.Returns((Device?)null);
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));
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_AlreadyApprovedOrRejected_ThrowsDuplicateAuthRequestException(
SutProvider<AuthRequestService> 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<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);
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));
}
}