1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05: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
7 changed files with 590 additions and 104 deletions

View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Auth.Enums;
namespace Bit.Core.Auth.Models.Api.Request.AuthRequest;
public class AuthRequestCreateRequestModel
{
[Required]
public string Email { get; set; }
[Required]
public string PublicKey { get; set; }
[Required]
public string DeviceIdentifier { get; set; }
[Required]
[StringLength(25)]
public string AccessCode { get; set; }
[Required]
public AuthRequestType? Type { 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;
}
}