mirror of
https://github.com/bitwarden/server.git
synced 2025-07-17 15:40:59 -05:00
[PM-20348] Add pending auth request endpoint (#5957)
* Feat(pm-20348): * Add migration scripts for Read Pending Auth Requests by UserId stored procedure and new `view` for pending AuthRequest. * View only returns the most recent pending authRequest, or none at all if the most recent is answered. * Implement stored procedure in AuthRequestRepository for both Dapper and Entity Framework. * Update AuthRequestController to query the new View to get a user's most recent pending auth requests response includes the requesting deviceId. * Doc: * Move summary xml comments to interface. * Added comments for the AuthRequestService. * Test: * Added testing for AuthRequestsController. * Added testing for repositories. * Added integration tests for multiple auth requests but only returning the most recent.
This commit is contained in:
83
src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs
Normal file
83
src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
|
||||
public class PendingAuthRequestDetails : AuthRequest
|
||||
{
|
||||
public Guid? RequestDeviceId { get; set; }
|
||||
|
||||
/**
|
||||
* Constructor for EF response.
|
||||
*/
|
||||
public PendingAuthRequestDetails(
|
||||
AuthRequest authRequest,
|
||||
Guid? deviceId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authRequest);
|
||||
|
||||
Id = authRequest.Id;
|
||||
UserId = authRequest.UserId;
|
||||
OrganizationId = authRequest.OrganizationId;
|
||||
Type = authRequest.Type;
|
||||
RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier;
|
||||
RequestDeviceType = authRequest.RequestDeviceType;
|
||||
RequestIpAddress = authRequest.RequestIpAddress;
|
||||
RequestCountryName = authRequest.RequestCountryName;
|
||||
ResponseDeviceId = authRequest.ResponseDeviceId;
|
||||
AccessCode = authRequest.AccessCode;
|
||||
PublicKey = authRequest.PublicKey;
|
||||
Key = authRequest.Key;
|
||||
MasterPasswordHash = authRequest.MasterPasswordHash;
|
||||
Approved = authRequest.Approved;
|
||||
CreationDate = authRequest.CreationDate;
|
||||
ResponseDate = authRequest.ResponseDate;
|
||||
AuthenticationDate = authRequest.AuthenticationDate;
|
||||
RequestDeviceId = deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for dapper response.
|
||||
*/
|
||||
public PendingAuthRequestDetails(
|
||||
Guid id,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
short type,
|
||||
string requestDeviceIdentifier,
|
||||
short requestDeviceType,
|
||||
string requestIpAddress,
|
||||
string requestCountryName,
|
||||
Guid? responseDeviceId,
|
||||
string accessCode,
|
||||
string publicKey,
|
||||
string key,
|
||||
string masterPasswordHash,
|
||||
bool? approved,
|
||||
DateTime creationDate,
|
||||
DateTime? responseDate,
|
||||
DateTime? authenticationDate,
|
||||
Guid deviceId)
|
||||
{
|
||||
Id = id;
|
||||
UserId = userId;
|
||||
OrganizationId = organizationId;
|
||||
Type = (AuthRequestType)type;
|
||||
RequestDeviceIdentifier = requestDeviceIdentifier;
|
||||
RequestDeviceType = (DeviceType)requestDeviceType;
|
||||
RequestIpAddress = requestIpAddress;
|
||||
RequestCountryName = requestCountryName;
|
||||
ResponseDeviceId = responseDeviceId;
|
||||
AccessCode = accessCode;
|
||||
PublicKey = publicKey;
|
||||
Key = key;
|
||||
MasterPasswordHash = masterPasswordHash;
|
||||
Approved = approved;
|
||||
CreationDate = creationDate;
|
||||
ResponseDate = responseDate;
|
||||
AuthenticationDate = authenticationDate;
|
||||
RequestDeviceId = deviceId;
|
||||
}
|
||||
}
|
@ -9,6 +9,13 @@ public interface IAuthRequestRepository : IRepository<AuthRequest, Guid>
|
||||
{
|
||||
Task<int> DeleteExpiredAsync(TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration);
|
||||
Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId);
|
||||
/// <summary>
|
||||
/// Gets all active pending auth requests for a user. Each auth request in the collection will be associated with a different
|
||||
/// device. It will be the most current request for the device.
|
||||
/// </summary>
|
||||
/// <param name="userId">UserId of the owner of the AuthRequests</param>
|
||||
/// <returns>a collection Auth request details or empty</returns>
|
||||
Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId);
|
||||
Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
|
||||
Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests);
|
||||
|
@ -1,5 +1,9 @@
|
||||
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.Settings;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -7,8 +11,41 @@ namespace Bit.Core.Auth.Services;
|
||||
|
||||
public interface IAuthRequestService
|
||||
{
|
||||
Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId);
|
||||
Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code);
|
||||
/// <summary>
|
||||
/// Fetches an authRequest by Id. Returns AuthRequest if AuthRequest.UserId mateches
|
||||
/// userId. Returns null if the user doesn't match or if the AuthRequest is not found.
|
||||
/// </summary>
|
||||
/// <param name="authRequestId">Authrequest Id being fetched</param>
|
||||
/// <param name="userId">user who owns AuthRequest</param>
|
||||
/// <returns>An AuthRequest or null</returns>
|
||||
Task<AuthRequest?> GetAuthRequestAsync(Guid authRequestId, Guid userId);
|
||||
/// <summary>
|
||||
/// Fetches the authrequest from the database with the id provided. Then checks
|
||||
/// the accessCode against the AuthRequest.AccessCode from the database. accessCodes
|
||||
/// must match the found authRequest, and the AuthRequest must not be expired. Expiration
|
||||
/// is configured in <see cref="GlobalSettings"/>
|
||||
/// </summary>
|
||||
/// <param name="authRequestId">AuthRequest being acted on</param>
|
||||
/// <param name="accessCode">Access code of the authrequest, must match saved database value</param>
|
||||
/// <returns>A valid AuthRequest or null</returns>
|
||||
Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid authRequestId, string accessCode);
|
||||
/// <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>
|
||||
Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model);
|
||||
/// <summary>
|
||||
/// Updates the AuthRequest per the AuthRequestUpdateRequestModel context. This approves
|
||||
/// or rejects the login request.
|
||||
/// </summary>
|
||||
/// <param name="authRequestId">AuthRequest being acted on.</param>
|
||||
/// <param name="userId">User acting on AuthRequest</param>
|
||||
/// <param name="model">Update context for the AuthRequest</param>
|
||||
/// <returns>retuns an AuthRequest or throws an exception</returns>
|
||||
/// <exception cref="DuplicateAuthRequestException">Thows if the AuthRequest has already been Approved/Rejected</exception>
|
||||
/// <exception cref="NotFoundException">Throws if the AuthRequest as expired or the userId doesn't match</exception>
|
||||
/// <exception cref="BadRequestException">Throws if the device isn't associated with the UserId</exception>
|
||||
Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model);
|
||||
}
|
||||
|
@ -58,9 +58,9 @@ public class AuthRequestService : IAuthRequestService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
|
||||
public async Task<AuthRequest?> GetAuthRequestAsync(Guid authRequestId, Guid userId)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(id);
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
|
||||
if (authRequest == null || authRequest.UserId != userId)
|
||||
{
|
||||
return null;
|
||||
@ -69,10 +69,10 @@ public class AuthRequestService : IAuthRequestService
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code)
|
||||
public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid authRequestId, string accessCode)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(id);
|
||||
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code))
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
|
||||
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, accessCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -85,12 +85,6 @@ public class AuthRequestService : IAuthRequestService
|
||||
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)
|
||||
{
|
||||
if (!_currentContext.DeviceType.HasValue)
|
||||
|
Reference in New Issue
Block a user