1
0
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:
Ike
2025-06-30 13:17:51 -04:00
committed by GitHub
parent 899ff1b660
commit 20bf1455cf
14 changed files with 752 additions and 50 deletions

View 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;
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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)