using Bit.Core; using Bit.Core.Auth.Identity; 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.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityModel; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using NSubstitute; using Xunit; namespace Bit.Identity.Test.IdentityServer.SendAccess; [SutProviderCustomize] public class SendAccessGrantValidatorTests { [Theory, BitAutoData] public async Task ValidateAsync_FeatureFlagDisabled_ReturnsUnsupportedGrantType( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.SendAccess) .Returns(false); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.True(context.Result.IsError); Assert.Equal(OidcConstants.TokenErrors.UnsupportedGrantType, context.Result.Error); } [Theory, BitAutoData] public async Task ValidateAsync_MissingSendId_ReturnsInvalidRequest( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.SendAccess) .Returns(true); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, context.Result.Error); Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is required.", context.Result.ErrorDescription); } [Theory, BitAutoData] public async Task ValidateAsync_InvalidSendId_ReturnsInvalidGrant( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.SendAccess) .Returns(true); var context = new ExtensionGrantValidationContext(); tokenRequest.GrantType = CustomGrantTypes.SendAccess; tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(Guid.Empty); // To preserve the CreateTokenRequestBody method for more general usage we over write the sendId tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format"); context.Request = tokenRequest; // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription); } [Theory, BitAutoData] public async Task ValidateAsync_EmptyGuidSendId_ReturnsInvalidGrant( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider) { // Arrange var context = SetupTokenRequest( sutProvider, Guid.Empty, // Empty Guid as sendId tokenRequest); // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription); } [Theory, BitAutoData] public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, NeverAuthenticate neverAuthenticate, Guid sendId, GrantValidationResult expectedResult) { // Arrange var context = SetupTokenRequest( sutProvider, sendId, tokenRequest); sutProvider.GetDependency() .GetAuthenticationMethod(sendId) .Returns(neverAuthenticate); sutProvider.GetDependency>() .ValidateRequestAsync(context, neverAuthenticate, sendId) .Returns(expectedResult); // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.Equal(expectedResult, context.Result); await sutProvider.GetDependency>() .Received(1) .ValidateRequestAsync(context, neverAuthenticate, sendId); } [Theory, BitAutoData] public async Task ValidateAsync_NotAuthenticatedMethod_ReturnsSuccess( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, Guid sendId) { // Arrange var context = SetupTokenRequest( sutProvider, sendId, tokenRequest); sutProvider.GetDependency() .GetAuthenticationMethod(sendId) .Returns(new NotAuthenticated()); // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.False(context.Result.IsError); // get the claims principal from the result var subject = context.Result.Subject; Assert.NotNull(subject); Assert.Equal(sendId.ToString(), subject.GetSubjectId()); Assert.Equal(CustomGrantTypes.SendAccess, subject.GetAuthenticationMethod()); // get the claims from the subject var claims = subject.Claims.ToList(); Assert.NotEmpty(claims); Assert.Contains(claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString()); Assert.Contains(claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); } [Theory, BitAutoData] public async Task ValidateAsync_ResourcePasswordMethod_CallsPasswordValidator( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, Guid sendId, ResourcePassword resourcePassword, GrantValidationResult expectedResult) { // Arrange var context = SetupTokenRequest( sutProvider, sendId, tokenRequest); sutProvider.GetDependency() .GetAuthenticationMethod(sendId) .Returns(resourcePassword); sutProvider.GetDependency>() .ValidateRequestAsync(context, resourcePassword, sendId) .Returns(expectedResult); // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.Equal(expectedResult, context.Result); await sutProvider.GetDependency>() .Received(1) .ValidateRequestAsync(context, resourcePassword, sendId); } [Theory, BitAutoData] public async Task ValidateAsync_EmailOtpMethod_CallsEmailOtp( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, GrantValidationResult expectedResult, Guid sendId, EmailOtp emailOtp) { // Arrange var context = SetupTokenRequest( sutProvider, sendId, tokenRequest); sutProvider.GetDependency() .GetAuthenticationMethod(sendId) .Returns(emailOtp); sutProvider.GetDependency>() .ValidateRequestAsync(context, emailOtp, sendId) .Returns(expectedResult); // Act await sutProvider.Sut.ValidateAsync(context); // Assert Assert.Equal(expectedResult, context.Result); await sutProvider.GetDependency>() .Received(1) .ValidateRequestAsync(context, emailOtp, sendId); } [Theory, BitAutoData] public async Task ValidateAsync_UnknownAuthMethod_ThrowsInvalidOperationException( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, Guid sendId) { // Arrange var context = SetupTokenRequest( sutProvider, sendId, tokenRequest); // Create a mock authentication method that's not handled var unknownMethod = Substitute.For(); sutProvider.GetDependency() .GetAuthenticationMethod(sendId) .Returns(unknownMethod); // Act & Assert var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ValidateAsync(context)); Assert.StartsWith("Unknown auth method:", exception.Message); } [Fact] public void GrantType_ReturnsCorrectType() { // Arrange & Act var validator = new SendAccessGrantValidator(null!, null!, null!, null!, null!); // Assert Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType); } /// /// Mutator method fo the SutProvider and the Context to set up a valid request /// /// current sut provider /// test context /// the send id /// the token request private static ExtensionGrantValidationContext SetupTokenRequest( SutProvider sutProvider, Guid sendId, ValidatedTokenRequest request) { sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.SendAccess) .Returns(true); var context = new ExtensionGrantValidationContext(); request.GrantType = CustomGrantTypes.SendAccess; request.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId); context.Request = request; return context; } }