using Bit.Core.KeyManagement.Sends; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.IntegrationTestCommon.Factories; using Duende.IdentityModel; using NSubstitute; using Xunit; namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; public class SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture { [Fact] public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken() { // Arrange var sendId = Guid.NewGuid(); var passwordHash = "stored-password-hash"; var clientPasswordHash = "client-password-hash"; var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Enable feature flag var featureService = Substitute.For(); featureService.IsEnabled(Arg.Any()).Returns(true); services.AddSingleton(featureService); // Mock send authentication query var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) .Returns(new ResourcePassword(passwordHash)); services.AddSingleton(sendAuthQuery); // Mock password hasher to return true for matching passwords var passwordHasher = Substitute.For(); passwordHasher.PasswordHashMatches(passwordHash, clientPasswordHash) .Returns(true); services.AddSingleton(passwordHasher); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: clientPasswordHash); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert Assert.True(response.IsSuccessStatusCode); var content = await response.Content.ReadAsStringAsync(); Assert.Contains(OidcConstants.TokenResponse.AccessToken, content); Assert.Contains("bearer", content.ToLower()); } [Fact] public async Task SendAccess_PasswordProtectedSend_InvalidPassword_ReturnsInvalidGrant() { // Arrange var sendId = Guid.NewGuid(); var passwordHash = "stored-password-hash"; var wrongClientPasswordHash = "wrong-client-password-hash"; var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(Arg.Any()).Returns(true); services.AddSingleton(featureService); var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) .Returns(new ResourcePassword(passwordHash)); services.AddSingleton(sendAuthQuery); // Mock password hasher to return false for wrong passwords var passwordHasher = Substitute.For(); passwordHasher.PasswordHashMatches(passwordHash, wrongClientPasswordHash) .Returns(false); services.AddSingleton(passwordHasher); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: wrongClientPasswordHash); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert var content = await response.Content.ReadAsStringAsync(); Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content); Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid", content); } [Fact] public async Task SendAccess_PasswordProtectedSend_MissingPassword_ReturnsInvalidRequest() { // Arrange var sendId = Guid.NewGuid(); var passwordHash = "stored-password-hash"; var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(Arg.Any()).Returns(true); services.AddSingleton(featureService); var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) .Returns(new ResourcePassword(passwordHash)); services.AddSingleton(sendAuthQuery); var passwordHasher = Substitute.For(); services.AddSingleton(passwordHasher); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No password // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert var content = await response.Content.ReadAsStringAsync(); Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content); } /// /// When the password has is empty or whitespace it doesn't get passed to the server when the request is made. /// This leads to an invalid request error since the absence of the password hash is considered a malformed request. /// In the case that the passwordB64Hash _is_ empty or whitespace it would be an invalid grant since the request /// has the correct shape. /// [Fact] public async Task SendAccess_PasswordProtectedSend_EmptyPassword_ReturnsInvalidRequest() { // Arrange var sendId = Guid.NewGuid(); var passwordHash = "stored-password-hash"; var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(Arg.Any()).Returns(true); services.AddSingleton(featureService); var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) .Returns(new ResourcePassword(passwordHash)); services.AddSingleton(sendAuthQuery); // Mock password hasher to return false for empty passwords var passwordHasher = Substitute.For(); passwordHasher.PasswordHashMatches(passwordHash, string.Empty) .Returns(false); services.AddSingleton(passwordHasher); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, string.Empty); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert var content = await response.Content.ReadAsStringAsync(); Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content); } }