mirror of
https://github.com/bitwarden/server
synced 2026-03-02 11:21:31 +00:00
[PM-22696] send enumeration protection (#6352)
* feat: add static enumeration helper class * test: add enumeration helper class unit tests * feat: implement NeverAuthenticateValidator * test: unit and integration tests SendNeverAuthenticateValidator * test: use static class for common integration test setup for Send Access unit and integration tests * test: update tests to use static helper
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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<IdentityApplicationFactory>
|
||||
{
|
||||
[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<IFeatureService>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
// Mock send authentication query
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = new FormUrlEncodedContent([
|
||||
new KeyValuePair<string, string>(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new KeyValuePair<string, string>(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<IFeatureService>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId).Returns(resourcePassword);
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock password validator to return success
|
||||
var passwordValidator = Substitute.For<ISendAuthenticationMethodValidator<ResourcePassword>>();
|
||||
passwordValidator.ValidateRequestAsync(
|
||||
Arg.Any<ExtensionGrantValidationContext>(),
|
||||
Arg.Any<ResourcePassword>(),
|
||||
Arg.Any<Guid>())
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
public static class SendAccessTestUtilities
|
||||
{
|
||||
public static FormUrlEncodedContent CreateTokenRequestBody(
|
||||
Guid sendId,
|
||||
string email = null,
|
||||
string emailOtp = null,
|
||||
string password = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("device_type", "10")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.Email, email));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(emailOtp))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.Otp, emailOtp));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(["test@example.com"]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No email
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains("email is required", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var generatedToken = "123456";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp([email]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(generatedToken);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
// Mock mail service
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains("email otp sent", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_ValidOtp_ReturnsAccessToken()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var otp = "123456";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate successfully
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.ValidateTokenAsync(otp, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(true);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: otp);
|
||||
|
||||
// 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(OidcConstants.TokenResponse.BearerTokenType, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_InvalidOtp_ReturnsInvalidGrant()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var invalidOtp = "wrong123";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate as false
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.ValidateTokenAsync(invalidOtp, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(false);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: invalidOtp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
Assert.Contains("email otp is invalid", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_OtpGenerationFails_ReturnsInvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to fail generation
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns((string)null);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
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 SendNeverAuthenticateRequestValidatorIntegrationTests(
|
||||
IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
/// <summary>
|
||||
/// To support the static hashing function <see cref="EnumerationProtectionHelpers.GetIndexForInputHash"/> theses GUIDs and Key must be hardcoded
|
||||
/// </summary>
|
||||
private static readonly string _testHashKey = "test-key-123456789012345678901234567890";
|
||||
// These Guids are static and ensure the correct index for each error type
|
||||
private static readonly Guid _invalidSendGuid = Guid.Parse("1b35fbf3-a14a-4d48-82b7-2adc34fdae6f");
|
||||
private static readonly Guid _emailSendGuid = Guid.Parse("bc8e2ef5-a0bd-44d2-bdb7-5902be6f5c41");
|
||||
private static readonly Guid _passwordSendGuid = Guid.Parse("da36fa27-f0e8-4701-a585-d3d8c2f67c4b");
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_NoParameters_ReturnsInvalidSendId()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_invalidSendGuid);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_invalidSendGuid);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
|
||||
var expectedError = SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_ReturnsEmailRequired()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_emailSendGuid);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// should be invalid grant
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
|
||||
// Try to compel the invalid email error
|
||||
var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailRequired;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_WithEmail_ReturnsEmailInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var client = ConfigureTestHttpClient(_emailSendGuid);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid, email: email);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// should be invalid grant
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
|
||||
// Try to compel the invalid email error
|
||||
var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailInvalid;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_ReturnsPasswordRequired()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_passwordSendGuid);
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_passwordSendGuid);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
|
||||
var expectedError = SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_WithPassword_ReturnsPasswordInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var password = "test-password-hash";
|
||||
|
||||
var client = ConfigureTestHttpClient(_passwordSendGuid);
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_passwordSendGuid, password: password);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
|
||||
var expectedError = SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_ConsistentResponse_SameSendId()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_emailSendGuid);
|
||||
|
||||
var requestBody1 = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid);
|
||||
var requestBody2 = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid);
|
||||
|
||||
// Act
|
||||
var response1 = await client.PostAsync("/connect/token", requestBody1);
|
||||
var response2 = await client.PostAsync("/connect/token", requestBody2);
|
||||
|
||||
// Assert
|
||||
var content1 = await response1.Content.ReadAsStringAsync();
|
||||
var content2 = await response2.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(content1, content2);
|
||||
}
|
||||
|
||||
private HttpClient ConfigureTestHttpClient(Guid sendId)
|
||||
{
|
||||
_factory.UpdateConfiguration(
|
||||
"globalSettings:sendDefaultHashKey", _testHashKey);
|
||||
return _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new NeverAuthenticate());
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
});
|
||||
}).CreateClient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
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<IdentityApplicationFactory>
|
||||
{
|
||||
[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<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
// Mock send authentication query
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new ResourcePassword(passwordHash));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock password hasher to return true for matching passwords
|
||||
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new ResourcePassword(passwordHash));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock password hasher to return false for wrong passwords
|
||||
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||
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<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new ResourcePassword(passwordHash));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new ResourcePassword(passwordHash));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock password hasher to return false for empty passwords
|
||||
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user