mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +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:
parent
f9038472ce
commit
5a850f48e2
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
14
src/Core/Auth/Services/IAuthRequestService.cs
Normal file
14
src/Core/Auth/Services/IAuthRequestService.cs
Normal 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);
|
||||
}
|
149
src/Core/Auth/Services/Implementations/AuthRequestService.cs
Normal file
149
src/Core/Auth/Services/Implementations/AuthRequestService.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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>();
|
||||
|
393
test/Core.Test/Auth/Services/AuthRequestServiceTests.cs
Normal file
393
test/Core.Test/Auth/Services/AuthRequestServiceTests.cs
Normal 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));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user