using Bit.Core; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.IntegrationTestCommon.Factories; using Duende.IdentityModel; using Duende.IdentityServer.Validation; using NSubstitute; using Xunit; namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; // in order to test the default case for the authentication method, we need to create a custom one so we can ensure the // method throws as expected. internal record AnUnknownAuthenticationMethod : SendAuthenticationMethod { } public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture { [Fact] public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType() { // Arrange var sendId = Guid.NewGuid(); var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Mock feature service to return false var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(false); services.AddSingleton(featureService); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert var content = await response.Content.ReadAsStringAsync(); Assert.Contains("unsupported_grant_type", content); } [Fact] public async Task SendAccessGrant_ValidNotAuthenticatedSend_ReturnsAccessToken() { // Arrange var sendId = Guid.NewGuid(); var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Mock feature service to return true var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); services.AddSingleton(featureService); // Mock send authentication query var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId).Returns(new NotAuthenticated()); services.AddSingleton(sendAuthQuery); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert Assert.True(response.IsSuccessStatusCode); var content = await response.Content.ReadAsStringAsync(); Assert.Contains("access_token", content); Assert.Contains("bearer", content.ToLower()); } [Fact] public async Task SendAccessGrant_MissingSendId_ReturnsInvalidRequest() { // Arrange var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); services.AddSingleton(featureService); }); }).CreateClient(); var requestBody = new FormUrlEncodedContent([ new KeyValuePair(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), new KeyValuePair(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send) ]); // 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.SendId} is required", content); } [Fact] public async Task SendAccessGrant_EmptySendGuid_ReturnsInvalidGrant() { // Arrange var sendId = Guid.Empty; var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); services.AddSingleton(featureService); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert var content = await response.Content.ReadAsStringAsync(); Assert.Contains("invalid_grant", content); } [Fact] public async Task SendAccessGrant_NeverAuthenticateSend_ReturnsInvalidGrant() { // Arrange var sendId = Guid.NewGuid(); var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); services.AddSingleton(featureService); var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId).Returns(new NeverAuthenticate()); services.AddSingleton(sendAuthQuery); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert var content = await response.Content.ReadAsStringAsync(); Assert.Contains("invalid_grant", content); } [Fact] public async Task SendAccessGrant_UnknownAuthenticationMethod_ThrowsInvalidOperation() { // Arrange var sendId = Guid.NewGuid(); var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); services.AddSingleton(featureService); var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId).Returns(new AnUnknownAuthenticationMethod()); services.AddSingleton(sendAuthQuery); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var error = await client.PostAsync("/connect/token", requestBody); // Assert // We want to parse the response and ensure we get the correct error from the server var content = await error.Content.ReadAsStringAsync(); Assert.Contains("invalid_grant", content); } [Fact] public async Task SendAccessGrant_PasswordProtectedSend_CallsPasswordValidator() { // Arrange var sendId = Guid.NewGuid(); var resourcePassword = new ResourcePassword("test-password-hash"); var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); services.AddSingleton(featureService); var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId).Returns(resourcePassword); services.AddSingleton(sendAuthQuery); // Mock password validator to return success var passwordValidator = Substitute.For>(); passwordValidator.ValidateRequestAsync( Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new GrantValidationResult( subject: sendId.ToString(), authenticationMethod: CustomGrantTypes.SendAccess)); services.AddSingleton(passwordValidator); }); }).CreateClient(); var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, "password123"); // Act var response = await client.PostAsync("/connect/token", requestBody); // Assert Assert.True(response.IsSuccessStatusCode); var content = await response.Content.ReadAsStringAsync(); Assert.Contains("access_token", content); Assert.Contains("Bearer", content); } }