diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index 142ff26df6..d1d6a8a524 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -52,12 +52,12 @@ public class AuthRequestsController( [HttpGet("pending")] [RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)] - public async Task> GetPendingAuthRequestsAsync() + public async Task> GetPendingAuthRequestsAsync() { var userId = _userService.GetProperUserId(User).Value; var rawResponse = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId); - var responses = rawResponse.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)); - return new ListResponseModel(responses); + var responses = rawResponse.Select(a => new PendingAuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)); + return new ListResponseModel(responses); } [HttpGet("{id}/response")] diff --git a/src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs new file mode 100644 index 0000000000..e7bfc3f974 --- /dev/null +++ b/src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs @@ -0,0 +1,15 @@ +using Bit.Core.Auth.Models.Data; + +namespace Bit.Api.Auth.Models.Response; + +public class PendingAuthRequestResponseModel : AuthRequestResponseModel +{ + public PendingAuthRequestResponseModel(PendingAuthRequestDetails authRequest, string vaultUri, string obj = "auth-request") + : base(authRequest, vaultUri, obj) + { + ArgumentNullException.ThrowIfNull(authRequest); + RequestingDeviceId = authRequest.DeviceId; + } + + public Guid? RequestingDeviceId { get; set; } +} diff --git a/src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs b/src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs new file mode 100644 index 0000000000..5229a97aea --- /dev/null +++ b/src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs @@ -0,0 +1,89 @@ + +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? DeviceId { 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; + DeviceId = deviceId; + } + + /** + * Constructor for dapper response. + * Note: if the DeviceId is null it comes back as an empty guid That could change if the stored + * procedure runs on a different kind of db. + * In order to maintain the flexibility of the wildcard * in SQL the constrctor accepts a long "row number rn" + * parameter that was used to order the results in the SQL query. Also SQL complains about the constructor not + * having the same parameters as the SELECT statement. + */ + public PendingAuthRequestDetails( + Guid id, + Guid userId, + short type, + string requestDeviceIdentifier, + short requestDeviceType, + string requestIpAddress, + Guid? responseDeviceId, + string accessCode, + string publicKey, + string key, + string masterPasswordHash, + DateTime creationDate, + DateTime? responseDate, + DateTime? authenticationDate, + bool? approved, + Guid organizationId, + string requestCountryName, + Guid deviceId, + long rn) // see comment above about rn parameter + { + 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; + DeviceId = deviceId; + } +} diff --git a/src/Core/Auth/Repositories/IAuthRequestRepository.cs b/src/Core/Auth/Repositories/IAuthRequestRepository.cs index 1f0e5a616f..ff375ee7b0 100644 --- a/src/Core/Auth/Repositories/IAuthRequestRepository.cs +++ b/src/Core/Auth/Repositories/IAuthRequestRepository.cs @@ -15,7 +15,7 @@ public interface IAuthRequestRepository : IRepository /// /// UserId of the owner of the AuthRequests /// a collection Auth request details or null - Task> GetManyPendingAuthRequestByUserId(Guid userId); + Task> GetManyPendingAuthRequestByUserId(Guid userId); Task> GetManyPendingByOrganizationIdAsync(Guid organizationId); Task> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable ids); Task UpdateManyAsync(IEnumerable authRequests); diff --git a/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs index c3d2c73225..c9cf796986 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs @@ -51,11 +51,11 @@ public class AuthRequestRepository : Repository, IAuthRequest } } - public async Task> GetManyPendingAuthRequestByUserId(Guid userId) + public async Task> GetManyPendingAuthRequestByUserId(Guid userId) { var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes; using var connection = new SqlConnection(ConnectionString); - var results = await connection.QueryAsync( + var results = await connection.QueryAsync( $"[{Schema}].[AuthRequest_ReadPendingByUserId]", new { UserId = userId, ExpirationMinutes = expirationMinutes }, commandType: CommandType.StoredProcedure); diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs index 987b19d94a..3037e70b8c 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs @@ -62,7 +62,7 @@ public class AuthRequestRepository : Repository> GetManyPendingAuthRequestByUserId(Guid userId) + public async Task> GetManyPendingAuthRequestByUserId(Guid userId) { var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes; using var scope = ServiceScopeFactory.CreateScope(); @@ -76,10 +76,12 @@ public class AuthRequestRepository : Repository a.Approved != null); return mostRecentAuthRequests; diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadPendingByUserId.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql similarity index 53% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadPendingByUserId.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql index db9a35f7d6..b3465f912c 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadPendingByUserId.sql +++ b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId] +CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId] @UserId UNIQUEIDENTIFIER, @ExpirationMinutes INT AS @@ -8,9 +8,12 @@ BEGIN ;WITH PendingRequests AS ( SELECT AR.*, - ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) AS rn + D.Id AS DeviceId, + ROW_NUMBER() OVER (PARTITION BY AR.RequestDeviceIdentifier ORDER BY AR.CreationDate DESC) AS rn FROM dbo.AuthRequestView AR - WHERE Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock + LEFT JOIN + Device D ON AR.RequestDeviceIdentifier = D.Identifier + WHERE AR.Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock AND AR.CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) AND AR.UserId = @UserId ) diff --git a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs index 20f391c609..828911f6bd 100644 --- a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs @@ -5,6 +5,7 @@ using Bit.Api.Models.Response; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.AuthRequest; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Services; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -100,7 +101,7 @@ public class AuthRequestsControllerTests public async Task GetPending_ReturnsExpectedResult( SutProvider sutProvider, User user, - AuthRequest authRequest) + PendingAuthRequestDetails authRequest) { // Arrange SetBaseServiceUri(sutProvider); @@ -120,7 +121,7 @@ public class AuthRequestsControllerTests Assert.NotNull(result); var expectedCount = 1; Assert.Equal(result.Data.Count(), expectedCount); - Assert.IsType>(result); + Assert.IsType>(result); } [Theory, BitAutoData] diff --git a/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql b/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql index 5d37e4d6b0..62edc9e91b 100644 --- a/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql +++ b/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql @@ -9,9 +9,12 @@ BEGIN ;WITH PendingRequests AS ( SELECT AR.*, - ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) AS rn + D.Id AS DeviceId, + ROW_NUMBER() OVER (PARTITION BY AR.RequestDeviceIdentifier ORDER BY AR.CreationDate DESC) AS rn FROM dbo.AuthRequestView AR - WHERE Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock + LEFT JOIN + Device D ON AR.RequestDeviceIdentifier = D.Identifier + WHERE AR.Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock AND AR.CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) AND AR.UserId = @UserId )