1
0
mirror of https://github.com/bitwarden/server synced 2026-01-05 18:13: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:
Ike
2025-09-23 06:38:22 -04:00
committed by GitHub
parent c6f5d5e36e
commit 3b54fea309
19 changed files with 989 additions and 290 deletions

View File

@@ -0,0 +1,230 @@
using System.Security.Cryptography;
using System.Text;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.Utilities;
public class EnumerationProtectionHelpersTests
{
#region GetIndexForInputHash Tests
[Fact]
public void GetIndexForInputHash_NullHmacKey_ReturnsZero()
{
// Arrange
byte[] hmacKey = null;
var salt = "test@example.com";
var range = 10;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetIndexForInputHash_ZeroRange_ReturnsZero()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
var range = 0;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetIndexForInputHash_NegativeRange_ReturnsZero()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
var range = -5;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetIndexForInputHash_ValidInputs_ReturnsConsistentResult()
{
// Arrange
var hmacKey = Encoding.UTF8.GetBytes("test-key-12345678901234567890123456789012");
var salt = "test@example.com";
var range = 10;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(result1, result2);
Assert.InRange(result1, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_SameInputSameKey_AlwaysReturnsSameResult()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "consistent@example.com";
var range = 100;
// Act - Call multiple times
var results = new int[10];
for (var i = 0; i < 10; i++)
{
results[i] = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
}
// Assert - All results should be identical
Assert.All(results, result => Assert.Equal(results[0], result));
Assert.All(results, result => Assert.InRange(result, 0, range - 1));
}
[Fact]
public void GetIndexForInputHash_DifferentInputsSameKey_ReturnsDifferentResults()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt1 = "user1@example.com";
var salt2 = "user2@example.com";
var range = 100;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt1, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt2, range);
// Assert
Assert.NotEqual(result1, result2);
Assert.InRange(result1, 0, range - 1);
Assert.InRange(result2, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_DifferentKeysSameInput_ReturnsDifferentResults()
{
// Arrange
var hmacKey1 = RandomNumberGenerator.GetBytes(32);
var hmacKey2 = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
var range = 100;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey1, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey2, salt, range);
// Assert
Assert.NotEqual(result1, result2);
Assert.InRange(result1, 0, range - 1);
Assert.InRange(result2, 0, range - 1);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(5)]
[InlineData(10)]
[InlineData(100)]
[InlineData(1000)]
public void GetIndexForInputHash_VariousRanges_ReturnsValidIndex(int range)
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.InRange(result, 0, range - 1);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void GetIndexForInputHash_EmptyString_HandlesGracefully(string salt)
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, 10);
// Assert
Assert.InRange(result, 0, 9);
}
[Fact]
public void GetIndexForInputHash_NullInput_ThrowsException()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
string salt = null;
var range = 10;
// Act & Assert
Assert.Throws<NullReferenceException>(() =>
EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range));
}
[Fact]
public void GetIndexForInputHash_SpecialCharacters_HandlesCorrectly()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test+user@example.com!@#$%^&*()";
var range = 50;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(result1, result2);
Assert.InRange(result1, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_UnicodeCharacters_HandlesCorrectly()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "tëst@éxämplé.cöm";
var range = 25;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(result1, result2);
Assert.InRange(result1, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_LongInput_HandlesCorrectly()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = new string('a', 1000) + "@example.com";
var range = 30;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.InRange(result, 0, range - 1);
}
#endregion
}

View File

@@ -1,10 +1,8 @@
using Bit.Core;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Enums;
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.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.IntegrationTestCommon.Factories;
@@ -13,16 +11,14 @@ using Duende.IdentityServer.Validation;
using NSubstitute;
using Xunit;
namespace Bit.Identity.IntegrationTest.RequestValidation;
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>
public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
{
private readonly IdentityApplicationFactory _factory = factory;
[Fact]
public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType()
{
@@ -39,7 +35,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -70,7 +66,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -125,7 +121,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -154,7 +150,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -183,7 +179,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
// Act
var error = await client.PostAsync("/connect/token", requestBody);
@@ -225,7 +221,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, "password123");
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, "password123");
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -236,37 +232,4 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
Assert.Contains("access_token", content);
Assert.Contains("Bearer", content);
}
private static FormUrlEncodedContent CreateTokenRequestBody(
Guid sendId,
string password = null,
string sendEmail = null,
string emailOtp = 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(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
};
if (!string.IsNullOrEmpty(password))
{
parameters.Add(new(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
}
if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail))
{
parameters.AddRange(
[
new KeyValuePair<string, string>("email", sendEmail),
new KeyValuePair<string, string>("email_otp", emailOtp)
]);
}
return new FormUrlEncodedContent(parameters);
}
}

View File

@@ -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);
}
}

View File

@@ -1,28 +1,16 @@
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Enums;
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.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.IntegrationTestCommon.Factories;
using Duende.IdentityModel;
using NSubstitute;
using Xunit;
namespace Bit.Identity.IntegrationTest.RequestValidation;
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
{
private readonly IdentityApplicationFactory _factory;
public SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
{
@@ -43,7 +31,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId); // No email
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No email
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -87,7 +75,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -130,7 +118,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: otp);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: otp);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -174,7 +162,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: invalidOtp);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: invalidOtp);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -216,7 +204,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -225,32 +213,4 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
var content = await response.Content.ReadAsStringAsync();
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
}
private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId,
string sendEmail = null, string emailOtp = 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(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
};
if (!string.IsNullOrEmpty(sendEmail))
{
parameters.Add(new KeyValuePair<string, string>(
SendAccessConstants.TokenRequest.Email, sendEmail));
}
if (!string.IsNullOrEmpty(emailOtp))
{
parameters.Add(new KeyValuePair<string, string>(
SendAccessConstants.TokenRequest.Otp, emailOtp));
}
return new FormUrlEncodedContent(parameters);
}
}

View File

@@ -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();
}
}

View File

@@ -1,28 +1,17 @@
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Sends;
using Bit.Core.KeyManagement.Sends;
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.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.IntegrationTestCommon.Factories;
using Duende.IdentityModel;
using NSubstitute;
using Xunit;
namespace Bit.Identity.IntegrationTest.RequestValidation;
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
public class SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
{
private readonly IdentityApplicationFactory _factory;
public SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken()
{
@@ -54,7 +43,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, clientPasswordHash);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: clientPasswordHash);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -95,7 +84,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, wrongClientPasswordHash);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: wrongClientPasswordHash);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -131,7 +120,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId); // No password
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No password
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -176,7 +165,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
});
}).CreateClient();
var requestBody = CreateTokenRequestBody(sendId, string.Empty);
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, string.Empty);
// Act
var response = await client.PostAsync("/connect/token", requestBody);
@@ -186,24 +175,4 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content);
}
private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId, string passwordHash = 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("deviceType", "10")
};
if (passwordHash != null)
{
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash));
}
return new FormUrlEncodedContent(parameters);
}
}

View File

@@ -12,7 +12,7 @@ using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.Identity;
using Xunit;
namespace Bit.Identity.IntegrationTest.RequestValidation;
namespace Bit.Identity.IntegrationTest.RequestValidation.VaultAccess;
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
{

View File

@@ -1,12 +1,8 @@
using System.Collections.Specialized;
using Bit.Core;
using Bit.Core;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Enums;
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.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.Test.Common.AutoFixture;
@@ -81,7 +77,7 @@ public class SendAccessGrantValidatorTests
var context = new ExtensionGrantValidationContext();
tokenRequest.GrantType = CustomGrantTypes.SendAccess;
tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty);
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");
@@ -118,7 +114,9 @@ public class SendAccessGrantValidatorTests
public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant(
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
SutProvider<SendAccessGrantValidator> sutProvider,
Guid sendId)
NeverAuthenticate neverAuthenticate,
Guid sendId,
GrantValidationResult expectedResult)
{
// Arrange
var context = SetupTokenRequest(
@@ -128,14 +126,20 @@ public class SendAccessGrantValidatorTests
sutProvider.GetDependency<ISendAuthenticationQuery>()
.GetAuthenticationMethod(sendId)
.Returns(new NeverAuthenticate());
.Returns(neverAuthenticate);
sutProvider.GetDependency<ISendAuthenticationMethodValidator<NeverAuthenticate>>()
.ValidateRequestAsync(context, neverAuthenticate, sendId)
.Returns(expectedResult);
// 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);
Assert.Equal(expectedResult, context.Result);
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<NeverAuthenticate>>()
.Received(1)
.ValidateRequestAsync(context, neverAuthenticate, sendId);
}
[Theory, BitAutoData]
@@ -264,7 +268,7 @@ public class SendAccessGrantValidatorTests
public void GrantType_ReturnsCorrectType()
{
// Arrange & Act
var validator = new SendAccessGrantValidator(null!, null!, null!, null!);
var validator = new SendAccessGrantValidator(null!, null!, null!, null!, null!);
// Assert
Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType);
@@ -289,44 +293,9 @@ public class SendAccessGrantValidatorTests
var context = new ExtensionGrantValidationContext();
request.GrantType = CustomGrantTypes.SendAccess;
request.Raw = CreateTokenRequestBody(sendId);
request.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
context.Request = request;
return context;
}
private static NameValueCollection CreateTokenRequestBody(
Guid sendId,
string passwordHash = null,
string sendEmail = null,
string otpCode = null)
{
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
var rawRequestParameters = new NameValueCollection
{
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
{ "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() },
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
};
if (passwordHash != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash);
}
if (sendEmail != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
}
if (otpCode != null && sendEmail != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
}
return rawRequestParameters;
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Specialized;
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.Test.IdentityServer.SendAccess;
public static class SendAccessTestUtilities
{
public static NameValueCollection CreateValidatedTokenRequest(
Guid sendId,
string sendEmail = null,
string otpCode = null,
params string[] passwordHash)
{
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
var rawRequestParameters = new NameValueCollection
{
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
};
if (sendEmail != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
}
if (otpCode != null && sendEmail != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
}
if (passwordHash != null && passwordHash.Length > 0)
{
foreach (var hash in passwordHash)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash);
}
}
return rawRequestParameters;
}
}

View File

@@ -31,9 +31,9 @@ public class SendConstantsSnapshotTests
public void GrantValidatorResults_Constants_HaveCorrectValues()
{
// Assert
Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid);
Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired);
Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId);
Assert.Equal("valid_send_guid", SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid);
Assert.Equal("send_id_required", SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired);
Assert.Equal("send_id_invalid", SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
}
[Fact]

View File

@@ -1,12 +1,7 @@
using System.Collections.Specialized;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;
using Bit.Identity.IdentityServer.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -28,7 +23,7 @@ public class SendEmailOtpRequestValidatorTests
Guid sendId)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
@@ -61,8 +56,7 @@ public class SendEmailOtpRequestValidatorTests
Guid sendId)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
var emailOTP = new EmailOtp(["user@test.dev"]);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
@@ -96,7 +90,7 @@ public class SendEmailOtpRequestValidatorTests
string generatedToken)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
@@ -144,7 +138,7 @@ public class SendEmailOtpRequestValidatorTests
string email)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
@@ -179,7 +173,7 @@ public class SendEmailOtpRequestValidatorTests
string otp)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, otp);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, otp);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
@@ -231,7 +225,7 @@ public class SendEmailOtpRequestValidatorTests
string invalidOtp)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, invalidOtp);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, invalidOtp);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
@@ -278,33 +272,4 @@ public class SendEmailOtpRequestValidatorTests
// Assert
Assert.NotNull(validator);
}
private static NameValueCollection CreateValidatedTokenRequest(
Guid sendId,
string sendEmail = null,
string otpCode = null)
{
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
var rawRequestParameters = new NameValueCollection
{
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
};
if (sendEmail != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
}
if (otpCode != null && sendEmail != null)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
}
return rawRequestParameters;
}
}

View File

@@ -0,0 +1,280 @@
using Bit.Core.Tools.Models.Data;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityModel;
using Duende.IdentityServer.Validation;
using Xunit;
namespace Bit.Identity.Test.IdentityServer.SendAccess;
[SutProviderCustomize]
public class SendNeverAuthenticateRequestValidatorTests
{
/// <summary>
/// To support the static hashing function <see cref="EnumerationProtectionHelpers.GetIndexForSaltHash"/> 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");
private static readonly NeverAuthenticate _authMethod = new();
[Theory, BitAutoData]
public async Task ValidateRequestAsync_GuidErrorSelected_ReturnsInvalidSendId(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Equal(SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, result.ErrorDescription);
var customResponse = result.CustomResponse as Dictionary<string, object>;
Assert.NotNull(customResponse);
Assert.Equal(
SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, customResponse[SendAccessConstants.SendAccessError]);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailInvalid(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
string email)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid, sendEmail: email);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, result.ErrorDescription);
var customResponse = result.CustomResponse as Dictionary<string, object>;
Assert.NotNull(customResponse);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, customResponse[SendAccessConstants.SendAccessError]);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_EmailErrorSelected_NoEmail_ReturnsEmailRequired(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, result.ErrorDescription);
var customResponse = result.CustomResponse as Dictionary<string, object>;
Assert.NotNull(customResponse);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, customResponse[SendAccessConstants.SendAccessError]);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_PasswordErrorSelected_HasPassword_ReturnsPasswordDoesNotMatch(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
string password)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid, passwordHash: password);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, result.ErrorDescription);
var customResponse = result.CustomResponse as Dictionary<string, object>;
Assert.NotNull(customResponse);
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, customResponse[SendAccessConstants.SendAccessError]);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_PasswordErrorSelected_NoPassword_ReturnsPasswordRequired(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, result.ErrorDescription);
var customResponse = result.CustomResponse as Dictionary<string, object>;
Assert.NotNull(customResponse);
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, customResponse[SendAccessConstants.SendAccessError]);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_NullHashKey_UsesEmptyKey(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
var context = new ExtensionGrantValidationContext { Request = tokenRequest };
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = null;
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_EmptyHashKey_UsesEmptyKey(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = "";
// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid);
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_ConsistentBehavior_SameSendIdSameResult(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
Guid sendId)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = "consistent-test-key-123456789012345678901234567890";
// Act
var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId);
var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId);
// Assert
Assert.Equal(result1.ErrorDescription, result2.ErrorDescription);
Assert.Equal(result1.Error, result2.Error);
var customResponse1 = result1.CustomResponse as Dictionary<string, object>;
var customResponse2 = result2.CustomResponse as Dictionary<string, object>;
Assert.Equal(customResponse1[SendAccessConstants.SendAccessError], customResponse2[SendAccessConstants.SendAccessError]);
}
[Theory, BitAutoData]
public async Task ValidateRequestAsync_DifferentSendIds_CanReturnDifferentResults(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
Guid sendId1,
Guid sendId2)
{
// Arrange
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId1);
var context = new ExtensionGrantValidationContext
{
Request = tokenRequest
};
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = "different-test-key-123456789012345678901234567890";
// Act
var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId1);
var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId2);
// Assert - Both should be errors
Assert.True(result1.IsError);
Assert.True(result2.IsError);
// Both should have valid error types
var validErrors = new[]
{
SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId,
SendAccessConstants.EmailOtpValidatorResults.EmailRequired,
SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired
};
Assert.Contains(result1.ErrorDescription, validErrors);
Assert.Contains(result2.ErrorDescription, validErrors);
}
[Fact]
public void Constructor_WithValidGlobalSettings_CreatesInstance()
{
// Arrange
var globalSettings = new Core.Settings.GlobalSettings
{
SendDefaultHashKey = "test-key-123456789012345678901234567890"
};
// Act
var validator = new SendNeverAuthenticateRequestValidator(globalSettings);
// Assert
Assert.NotNull(validator);
}
}

View File

@@ -1,12 +1,7 @@
using System.Collections.Specialized;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.UserFeatures.SendAccess;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Sends;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;
using Bit.Identity.IdentityServer.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -28,7 +23,7 @@ public class SendPasswordRequestValidatorTests
Guid sendId)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
var context = new ExtensionGrantValidationContext
{
@@ -58,7 +53,7 @@ public class SendPasswordRequestValidatorTests
string clientPasswordHash)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
var context = new ExtensionGrantValidationContext
{
@@ -92,7 +87,7 @@ public class SendPasswordRequestValidatorTests
string clientPasswordHash)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
var context = new ExtensionGrantValidationContext
{
@@ -130,7 +125,7 @@ public class SendPasswordRequestValidatorTests
Guid sendId)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: string.Empty);
var context = new ExtensionGrantValidationContext
{
@@ -163,7 +158,7 @@ public class SendPasswordRequestValidatorTests
{
// Arrange
var whitespacePassword = " ";
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: whitespacePassword);
var context = new ExtensionGrantValidationContext
{
@@ -196,7 +191,7 @@ public class SendPasswordRequestValidatorTests
// Arrange
var firstPassword = "first-password";
var secondPassword = "second-password";
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: [firstPassword, secondPassword]);
var context = new ExtensionGrantValidationContext
{
@@ -229,7 +224,7 @@ public class SendPasswordRequestValidatorTests
string clientPasswordHash)
{
// Arrange
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
var context = new ExtensionGrantValidationContext
{
@@ -268,30 +263,4 @@ public class SendPasswordRequestValidatorTests
// Assert
Assert.NotNull(validator);
}
private static NameValueCollection CreateValidatedTokenRequest(
Guid sendId,
params string[] passwordHash)
{
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
var rawRequestParameters = new NameValueCollection
{
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
};
if (passwordHash != null && passwordHash.Length > 0)
{
foreach (var hash in passwordHash)
{
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash);
}
}
return rawRequestParameters;
}
}