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:
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.Api.Models.Response;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
|
||||||
using Bit.Core.Auth.Exceptions;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -18,30 +15,21 @@ namespace Bit.Api.Auth.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AuthRequestsController : Controller
|
public class AuthRequestsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IAuthRequestRepository _authRequestRepository;
|
private readonly IAuthRequestRepository _authRequestRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
private readonly IAuthRequestService _authRequestService;
|
||||||
|
|
||||||
public AuthRequestsController(
|
public AuthRequestsController(
|
||||||
IUserRepository userRepository,
|
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
ICurrentContext currentContext,
|
IGlobalSettings globalSettings,
|
||||||
IPushNotificationService pushNotificationService,
|
IAuthRequestService authRequestService)
|
||||||
IGlobalSettings globalSettings)
|
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
|
||||||
_deviceRepository = deviceRepository;
|
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_authRequestRepository = authRequestRepository;
|
_authRequestRepository = authRequestRepository;
|
||||||
_currentContext = currentContext;
|
|
||||||
_pushNotificationService = pushNotificationService;
|
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_authRequestService = authRequestService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -54,11 +42,12 @@ public class AuthRequestsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<AuthRequestResponseModel> Get(string id)
|
public async Task<AuthRequestResponseModel> Get(Guid id)
|
||||||
{
|
{
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
var authRequest = await _authRequestService.GetAuthRequestAsync(id, userId);
|
||||||
if (authRequest == null || authRequest.UserId != userId)
|
|
||||||
|
if (authRequest == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -68,10 +57,11 @@ public class AuthRequestsController : Controller
|
|||||||
|
|
||||||
[HttpGet("{id}/response")]
|
[HttpGet("{id}/response")]
|
||||||
[AllowAnonymous]
|
[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));
|
var authRequest = await _authRequestService.GetValidatedAuthRequestAsync(id, code);
|
||||||
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code) || authRequest.GetExpirationDate() < DateTime.UtcNow)
|
|
||||||
|
if (authRequest == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -83,80 +73,16 @@ public class AuthRequestsController : Controller
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
|
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByEmailAsync(model.Email);
|
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
|
||||||
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 r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[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 userId = _userService.GetProperUserId(User).Value;
|
||||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
var authRequest = await _authRequestService.UpdateAuthRequestAsync(id, userId, model);
|
||||||
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 new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Auth.Enums;
|
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
|
public class AuthRequestCreateRequestModel
|
||||||
{
|
{
|
||||||
@ -18,13 +17,3 @@ public class AuthRequestCreateRequestModel
|
|||||||
[Required]
|
[Required]
|
||||||
public AuthRequestType? Type { get; set; }
|
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.LoginFeatures;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
|
using Bit.Core.Auth.Services.Implementations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.HostedServices;
|
using Bit.Core.HostedServices;
|
||||||
@ -130,6 +131,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<IDeviceService, DeviceService>();
|
services.AddSingleton<IDeviceService, DeviceService>();
|
||||||
services.AddSingleton<IAppleIapService, AppleIapService>();
|
services.AddSingleton<IAppleIapService, AppleIapService>();
|
||||||
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
||||||
|
services.AddScoped<IAuthRequestService, AuthRequestService>();
|
||||||
services.AddScoped<ISendService, SendService>();
|
services.AddScoped<ISendService, SendService>();
|
||||||
services.AddLoginServices();
|
services.AddLoginServices();
|
||||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
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