diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs
index c6c96cffb9..107fd29236 100644
--- a/src/Core/Settings/GlobalSettings.cs
+++ b/src/Core/Settings/GlobalSettings.cs
@@ -92,6 +92,10 @@ public class GlobalSettings : IGlobalSettings
public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5;
public virtual bool EnableEmailVerification { get; set; }
public virtual string KdfDefaultHashKey { get; set; }
+ ///
+ /// This Hash Key is used to prevent enumeration attacks against the Send Access feature.
+ ///
+ public virtual string SendDefaultHashKey { get; set; }
public virtual string PricingUri { get; set; }
public string BuildExternalUri(string explicitValue, string name)
diff --git a/src/Core/Utilities/EnumerationProtectionHelpers.cs b/src/Core/Utilities/EnumerationProtectionHelpers.cs
new file mode 100644
index 0000000000..b27c36e03a
--- /dev/null
+++ b/src/Core/Utilities/EnumerationProtectionHelpers.cs
@@ -0,0 +1,36 @@
+using System.Text;
+
+namespace Bit.Core.Utilities;
+
+public static class EnumerationProtectionHelpers
+{
+ ///
+ /// Use this method to get a consistent int result based on the inputString that is in the range.
+ /// The same inputString will always return the same index result based on range input.
+ ///
+ /// Key used to derive the HMAC hash. Use a different key for each usage for optimal security
+ /// The string to derive an index result
+ /// The range of possible index values
+ /// An int between 0 and range - 1
+ public static int GetIndexForInputHash(byte[] hmacKey, string inputString, int range)
+ {
+ if (hmacKey == null || range <= 0 || hmacKey.Length == 0)
+ {
+ return 0;
+ }
+ else
+ {
+ // Compute the HMAC hash of the salt
+ var hmacMessage = Encoding.UTF8.GetBytes(inputString.Trim().ToLowerInvariant());
+ using var hmac = new System.Security.Cryptography.HMACSHA256(hmacKey);
+ var hmacHash = hmac.ComputeHash(hmacMessage);
+ // Convert the hash to a number
+ var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
+ var hashFirst8Bytes = hashHex[..16];
+ var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
+ // Find the default KDF value for this hash number
+ var hashIndex = (int)(Math.Abs(hashNumber) % range);
+ return hashIndex;
+ }
+ }
+}
diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs
index 17ec387411..1f5bfba244 100644
--- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs
+++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs
@@ -34,18 +34,18 @@ public static class SendAccessConstants
public const string Otp = "otp";
}
- public static class GrantValidatorResults
+ public static class SendIdGuidValidatorResults
{
///
- /// The sendId is valid and the request is well formed. Not returned in any response.
+ /// The in the request is a valid GUID and the request is well formed. Not returned in any response.
///
public const string ValidSendGuid = "valid_send_guid";
///
- /// The sendId is missing from the request.
+ /// The is missing from the request.
///
public const string SendIdRequired = "send_id_required";
///
- /// The sendId is invalid, does not match a known send.
+ /// The is invalid, does not match a known send.
///
public const string InvalidSendId = "send_id_invalid";
}
@@ -53,11 +53,11 @@ public static class SendAccessConstants
public static class PasswordValidatorResults
{
///
- /// The passwordHashB64 does not match the send's password hash.
+ /// The does not match the send's password hash.
///
public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid";
///
- /// The passwordHashB64 is missing from the request.
+ /// The is missing from the request.
///
public const string RequestPasswordIsRequired = "password_hash_b64_required";
}
@@ -105,4 +105,14 @@ public static class SendAccessConstants
{
public const string Subject = "Your Bitwarden Send verification code is {0}";
}
+
+ ///
+ /// We use these static strings to help guide the enumeration protection logic.
+ ///
+ public static class EnumerationProtection
+ {
+ public const string Guid = "guid";
+ public const string Password = "password";
+ public const string Email = "email";
+ }
}
diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs
index d9ae946d16..101c6952f3 100644
--- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs
+++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs
@@ -13,21 +13,19 @@ namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
public class SendAccessGrantValidator(
ISendAuthenticationQuery _sendAuthenticationQuery,
+ ISendAuthenticationMethodValidator _sendNeverAuthenticateValidator,
ISendAuthenticationMethodValidator _sendPasswordRequestValidator,
ISendAuthenticationMethodValidator _sendEmailOtpRequestValidator,
- IFeatureService _featureService)
-: IExtensionGrantValidator
+ IFeatureService _featureService) : IExtensionGrantValidator
{
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
- private static readonly Dictionary
- _sendGrantValidatorErrorDescriptions = new()
+ private static readonly Dictionary _sendGrantValidatorErrorDescriptions = new()
{
- { SendAccessConstants.GrantValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." },
- { SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
+ { SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." },
+ { SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
};
-
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// Check the feature flag
@@ -38,7 +36,7 @@ public class SendAccessGrantValidator(
}
var (sendIdGuid, result) = GetRequestSendId(context);
- if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid)
+ if (result != SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid)
{
context.Result = BuildErrorResult(result);
return;
@@ -49,15 +47,10 @@ public class SendAccessGrantValidator(
switch (method)
{
- case NeverAuthenticate:
+ case NeverAuthenticate never:
// null send scenario.
- // TODO PM-22675: Add send enumeration protection here (primarily benefits self hosted instances).
- // We should only map to password or email + OTP protected.
- // If user submits password guess for a falsely protected send, then we will return invalid password.
- // If user submits email + OTP guess for a falsely protected send, then we will return email sent, do not actually send an email.
- context.Result = BuildErrorResult(SendAccessConstants.GrantValidatorResults.InvalidSendId);
+ context.Result = await _sendNeverAuthenticateValidator.ValidateRequestAsync(context, never, sendIdGuid);
return;
-
case NotAuthenticated:
// automatically issue access token
context.Result = BuildBaseSuccessResult(sendIdGuid);
@@ -90,7 +83,7 @@ public class SendAccessGrantValidator(
// if the sendId is null then the request is the wrong shape and the request is invalid
if (sendId == null)
{
- return (Guid.Empty, SendAccessConstants.GrantValidatorResults.SendIdRequired);
+ return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired);
}
// the send_id is not null so the request is the correct shape, so we will attempt to parse it
try
@@ -100,20 +93,20 @@ public class SendAccessGrantValidator(
// Guid.Empty indicates an invalid send_id return invalid grant
if (sendGuid == Guid.Empty)
{
- return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
+ return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
}
- return (sendGuid, SendAccessConstants.GrantValidatorResults.ValidSendGuid);
+ return (sendGuid, SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid);
}
catch
{
- return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
+ return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
}
}
///
/// Builds an error result for the specified error type.
///
- /// This error is a constant string from
+ /// This error is a constant string from
/// The error result.
private static GrantValidationResult BuildErrorResult(string error)
{
@@ -125,12 +118,12 @@ public class SendAccessGrantValidator(
return error switch
{
// Request is the wrong shape
- SendAccessConstants.GrantValidatorResults.SendIdRequired => new GrantValidationResult(
+ SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired => new GrantValidationResult(
TokenRequestErrors.InvalidRequest,
errorDescription: _sendGrantValidatorErrorDescriptions[error],
customResponse),
// Request is correct shape but data is bad
- SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult(
+ SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId => new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
errorDescription: _sendGrantValidatorErrorDescriptions[error],
customResponse),
diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs
new file mode 100644
index 0000000000..36e033360f
--- /dev/null
+++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs
@@ -0,0 +1,87 @@
+using System.Text;
+using Bit.Core.Settings;
+using Bit.Core.Tools.Models.Data;
+using Bit.Core.Utilities;
+using Duende.IdentityServer.Models;
+using Duende.IdentityServer.Validation;
+
+namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
+
+///
+/// This class is used to protect our system from enumeration attacks. This Validator will always return an error result.
+/// We hash the SendId Guid passed into the request to select the an error from the list of possible errors. This ensures
+/// that the same error is always returned for the same SendId.
+///
+/// We need access to a hash key to generate the error index.
+public class SendNeverAuthenticateRequestValidator(GlobalSettings globalSettings) : ISendAuthenticationMethodValidator
+{
+ private readonly string[] _errorOptions =
+ [
+ SendAccessConstants.EnumerationProtection.Guid,
+ SendAccessConstants.EnumerationProtection.Password,
+ SendAccessConstants.EnumerationProtection.Email
+ ];
+
+ public Task ValidateRequestAsync(
+ ExtensionGrantValidationContext context,
+ NeverAuthenticate authMethod,
+ Guid sendId)
+ {
+ var neverAuthenticateError = GetErrorIndex(sendId, _errorOptions.Length);
+ var request = context.Request.Raw;
+ var errorType = neverAuthenticateError;
+
+ switch (neverAuthenticateError)
+ {
+ case SendAccessConstants.EnumerationProtection.Guid:
+ errorType = SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId;
+ break;
+ case SendAccessConstants.EnumerationProtection.Email:
+ var hasEmail = request.Get(SendAccessConstants.TokenRequest.Email) is not null;
+ errorType = hasEmail ? SendAccessConstants.EmailOtpValidatorResults.EmailInvalid
+ : SendAccessConstants.EmailOtpValidatorResults.EmailRequired;
+ break;
+ case SendAccessConstants.EnumerationProtection.Password:
+ var hasPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword) is not null;
+ errorType = hasPassword ? SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch
+ : SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired;
+ break;
+ }
+
+ return Task.FromResult(BuildErrorResult(errorType));
+ }
+
+ private static GrantValidationResult BuildErrorResult(string errorType)
+ {
+ // Create error response with custom response data
+ var customResponse = new Dictionary
+ {
+ { SendAccessConstants.SendAccessError, errorType }
+ };
+
+ var requestError = errorType switch
+ {
+ SendAccessConstants.EnumerationProtection.Guid => TokenRequestErrors.InvalidGrant,
+ SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired => TokenRequestErrors.InvalidGrant,
+ SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch => TokenRequestErrors.InvalidRequest,
+ SendAccessConstants.EmailOtpValidatorResults.EmailInvalid => TokenRequestErrors.InvalidGrant,
+ SendAccessConstants.EmailOtpValidatorResults.EmailRequired => TokenRequestErrors.InvalidRequest,
+ _ => TokenRequestErrors.InvalidGrant
+ };
+
+ return new GrantValidationResult(requestError, errorType, customResponse);
+ }
+
+ private string GetErrorIndex(Guid sendId, int range)
+ {
+ var salt = sendId.ToString();
+ byte[] hmacKey = [];
+ if (CoreHelpers.SettingHasValue(globalSettings.SendDefaultHashKey))
+ {
+ hmacKey = Encoding.UTF8.GetBytes(globalSettings.SendDefaultHashKey);
+ }
+
+ var index = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
+ return _errorOptions[index];
+ }
+}
diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs
index 9d062e5c06..e9056d030e 100644
--- a/src/Identity/Utilities/ServiceCollectionExtensions.cs
+++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs
@@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions
services.AddTransient();
services.AddTransient, SendPasswordRequestValidator>();
services.AddTransient, SendEmailOtpRequestValidator>();
+ services.AddTransient, SendNeverAuthenticateRequestValidator>();
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
var identityServerBuilder = services
diff --git a/test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs b/test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs
new file mode 100644
index 0000000000..68ac8af5d0
--- /dev/null
+++ b/test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs
@@ -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(() =>
+ 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
+}
diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessGrantValidatorIntegrationTests.cs
similarity index 81%
rename from test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs
rename to test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessGrantValidatorIntegrationTests.cs
index ca6417d49c..a6dd4b6b38 100644
--- a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs
+++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessGrantValidatorIntegrationTests.cs
@@ -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
+public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture
{
- 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>
- {
- 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("email", sendEmail),
- new KeyValuePair("email_otp", emailOtp)
- ]);
- }
-
- return new FormUrlEncodedContent(parameters);
- }
}
diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs
new file mode 100644
index 0000000000..7842bf6367
--- /dev/null
+++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs
@@ -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>
+ {
+ 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(SendAccessConstants.TokenRequest.Email, email));
+ }
+
+ if (!string.IsNullOrEmpty(emailOtp))
+ {
+ parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.Otp, emailOtp));
+ }
+
+ if (!string.IsNullOrEmpty(password))
+ {
+ parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
+ }
+
+ return new FormUrlEncodedContent(parameters);
+ }
+}
diff --git a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs
similarity index 79%
rename from test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs
rename to test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs
index 9a097cc061..3c4657653b 100644
--- a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs
+++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs
@@ -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
+public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture
{
- 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>
- {
- 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(
- SendAccessConstants.TokenRequest.Email, sendEmail));
- }
-
- if (!string.IsNullOrEmpty(emailOtp))
- {
- parameters.Add(new KeyValuePair(
- SendAccessConstants.TokenRequest.Otp, emailOtp));
- }
-
- return new FormUrlEncodedContent(parameters);
- }
}
diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs
new file mode 100644
index 0000000000..a81b01a293
--- /dev/null
+++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs
@@ -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
+{
+ ///
+ /// To support the static hashing function theses GUIDs and Key must be hardcoded
+ ///
+ 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();
+ featureService.IsEnabled(Arg.Any()).Returns(true);
+ services.AddSingleton(featureService);
+
+ var sendAuthQuery = Substitute.For();
+ sendAuthQuery.GetAuthenticationMethod(sendId)
+ .Returns(new NeverAuthenticate());
+ services.AddSingleton(sendAuthQuery);
+ });
+ }).CreateClient();
+ }
+}
diff --git a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendPasswordRequestValidatorIntegrationTests.cs
similarity index 80%
rename from test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs
rename to test/Identity.IntegrationTest/RequestValidation/SendAccess/SendPasswordRequestValidatorIntegrationTests.cs
index 856ffe1f6e..5b03a298ed 100644
--- a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs
+++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendPasswordRequestValidatorIntegrationTests.cs
@@ -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
+public class SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture
{
- 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>
- {
- 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(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash));
- }
-
- return new FormUrlEncodedContent(parameters);
- }
}
diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs
similarity index 99%
rename from test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs
rename to test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs
index 4d243906af..91123b3a60 100644
--- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs
+++ b/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs
@@ -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
{
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs
index 017ad70354..59d8dee2e2 100644
--- a/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs
@@ -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 sutProvider,
- Guid sendId)
+ NeverAuthenticate neverAuthenticate,
+ Guid sendId,
+ GrantValidationResult expectedResult)
{
// Arrange
var context = SetupTokenRequest(
@@ -128,14 +126,20 @@ public class SendAccessGrantValidatorTests
sutProvider.GetDependency()
.GetAuthenticationMethod(sendId)
- .Returns(new NeverAuthenticate());
+ .Returns(neverAuthenticate);
+
+ sutProvider.GetDependency>()
+ .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>()
+ .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;
- }
}
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs b/test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs
new file mode 100644
index 0000000000..b05380902a
--- /dev/null
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs
@@ -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;
+ }
+}
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs
index 95a0a6675b..96a097a53c 100644
--- a/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs
@@ -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]
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs
index 70a1585d8b..46f61cb333 100644
--- a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs
@@ -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;
- }
}
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs
new file mode 100644
index 0000000000..ae0434af83
--- /dev/null
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs
@@ -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
+{
+ ///
+ /// To support the static hashing function theses GUIDs and Key must be hardcoded
+ ///
+ 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 sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+
+ sutProvider.GetDependency().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;
+ Assert.NotNull(customResponse);
+ Assert.Equal(
+ SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, customResponse[SendAccessConstants.SendAccessError]);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailInvalid(
+ SutProvider sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
+ string email)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid, sendEmail: email);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+ sutProvider.GetDependency().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;
+ Assert.NotNull(customResponse);
+ Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, customResponse[SendAccessConstants.SendAccessError]);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateRequestAsync_EmailErrorSelected_NoEmail_ReturnsEmailRequired(
+ SutProvider sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+ sutProvider.GetDependency().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;
+ Assert.NotNull(customResponse);
+ Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, customResponse[SendAccessConstants.SendAccessError]);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateRequestAsync_PasswordErrorSelected_HasPassword_ReturnsPasswordDoesNotMatch(
+ SutProvider sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
+ string password)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid, passwordHash: password);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+ sutProvider.GetDependency().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;
+ Assert.NotNull(customResponse);
+ Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, customResponse[SendAccessConstants.SendAccessError]);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateRequestAsync_PasswordErrorSelected_NoPassword_ReturnsPasswordRequired(
+ SutProvider sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
+ {
+ // Arrange
+
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+ sutProvider.GetDependency().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;
+ Assert.NotNull(customResponse);
+ Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, customResponse[SendAccessConstants.SendAccessError]);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateRequestAsync_NullHashKey_UsesEmptyKey(
+ SutProvider sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
+ var context = new ExtensionGrantValidationContext { Request = tokenRequest };
+ sutProvider.GetDependency().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 sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+
+ sutProvider.GetDependency().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 sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
+ Guid sendId)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+
+ sutProvider.GetDependency().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;
+ var customResponse2 = result2.CustomResponse as Dictionary;
+ Assert.Equal(customResponse1[SendAccessConstants.SendAccessError], customResponse2[SendAccessConstants.SendAccessError]);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateRequestAsync_DifferentSendIds_CanReturnDifferentResults(
+ SutProvider sutProvider,
+ [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
+ Guid sendId1,
+ Guid sendId2)
+ {
+ // Arrange
+ tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId1);
+ var context = new ExtensionGrantValidationContext
+ {
+ Request = tokenRequest
+ };
+
+ sutProvider.GetDependency().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);
+ }
+}
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs
index e77626d37b..460b033fa7 100644
--- a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs
@@ -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;
- }
}