mirror of
https://github.com/bitwarden/server.git
synced 2025-12-11 22:15:45 -06: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:
parent
c6f5d5e36e
commit
3b54fea309
@ -92,6 +92,10 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5;
|
public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5;
|
||||||
public virtual bool EnableEmailVerification { get; set; }
|
public virtual bool EnableEmailVerification { get; set; }
|
||||||
public virtual string KdfDefaultHashKey { get; set; }
|
public virtual string KdfDefaultHashKey { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This Hash Key is used to prevent enumeration attacks against the Send Access feature.
|
||||||
|
/// </summary>
|
||||||
|
public virtual string SendDefaultHashKey { get; set; }
|
||||||
public virtual string PricingUri { get; set; }
|
public virtual string PricingUri { get; set; }
|
||||||
|
|
||||||
public string BuildExternalUri(string explicitValue, string name)
|
public string BuildExternalUri(string explicitValue, string name)
|
||||||
|
|||||||
36
src/Core/Utilities/EnumerationProtectionHelpers.cs
Normal file
36
src/Core/Utilities/EnumerationProtectionHelpers.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities;
|
||||||
|
|
||||||
|
public static class EnumerationProtectionHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hmacKey">Key used to derive the HMAC hash. Use a different key for each usage for optimal security</param>
|
||||||
|
/// <param name="inputString">The string to derive an index result</param>
|
||||||
|
/// <param name="range">The range of possible index values</param>
|
||||||
|
/// <returns>An int between 0 and range - 1</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,18 +34,18 @@ public static class SendAccessConstants
|
|||||||
public const string Otp = "otp";
|
public const string Otp = "otp";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GrantValidatorResults
|
public static class SendIdGuidValidatorResults
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sendId is valid and the request is well formed. Not returned in any response.
|
/// The <see cref="TokenRequest.SendId"/> in the request is a valid GUID and the request is well formed. Not returned in any response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ValidSendGuid = "valid_send_guid";
|
public const string ValidSendGuid = "valid_send_guid";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sendId is missing from the request.
|
/// The <see cref="TokenRequest.SendId"/> is missing from the request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SendIdRequired = "send_id_required";
|
public const string SendIdRequired = "send_id_required";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sendId is invalid, does not match a known send.
|
/// The <see cref="TokenRequest.SendId"/> is invalid, does not match a known send.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string InvalidSendId = "send_id_invalid";
|
public const string InvalidSendId = "send_id_invalid";
|
||||||
}
|
}
|
||||||
@ -53,11 +53,11 @@ public static class SendAccessConstants
|
|||||||
public static class PasswordValidatorResults
|
public static class PasswordValidatorResults
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The passwordHashB64 does not match the send's password hash.
|
/// The <see cref="TokenRequest.ClientB64HashedPassword"/> does not match the send's password hash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid";
|
public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The passwordHashB64 is missing from the request.
|
/// The <see cref="TokenRequest.ClientB64HashedPassword"/> is missing from the request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RequestPasswordIsRequired = "password_hash_b64_required";
|
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}";
|
public const string Subject = "Your Bitwarden Send verification code is {0}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We use these static strings to help guide the enumeration protection logic.
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumerationProtection
|
||||||
|
{
|
||||||
|
public const string Guid = "guid";
|
||||||
|
public const string Password = "password";
|
||||||
|
public const string Email = "email";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,21 +13,19 @@ namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
|||||||
|
|
||||||
public class SendAccessGrantValidator(
|
public class SendAccessGrantValidator(
|
||||||
ISendAuthenticationQuery _sendAuthenticationQuery,
|
ISendAuthenticationQuery _sendAuthenticationQuery,
|
||||||
|
ISendAuthenticationMethodValidator<NeverAuthenticate> _sendNeverAuthenticateValidator,
|
||||||
ISendAuthenticationMethodValidator<ResourcePassword> _sendPasswordRequestValidator,
|
ISendAuthenticationMethodValidator<ResourcePassword> _sendPasswordRequestValidator,
|
||||||
ISendAuthenticationMethodValidator<EmailOtp> _sendEmailOtpRequestValidator,
|
ISendAuthenticationMethodValidator<EmailOtp> _sendEmailOtpRequestValidator,
|
||||||
IFeatureService _featureService)
|
IFeatureService _featureService) : IExtensionGrantValidator
|
||||||
: IExtensionGrantValidator
|
|
||||||
{
|
{
|
||||||
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
|
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
|
||||||
|
|
||||||
private static readonly Dictionary<string, string>
|
private static readonly Dictionary<string, string> _sendGrantValidatorErrorDescriptions = new()
|
||||||
_sendGrantValidatorErrorDescriptions = new()
|
|
||||||
{
|
{
|
||||||
{ SendAccessConstants.GrantValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." },
|
{ SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." },
|
||||||
{ SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
|
{ SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
public async Task ValidateAsync(ExtensionGrantValidationContext context)
|
public async Task ValidateAsync(ExtensionGrantValidationContext context)
|
||||||
{
|
{
|
||||||
// Check the feature flag
|
// Check the feature flag
|
||||||
@ -38,7 +36,7 @@ public class SendAccessGrantValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (sendIdGuid, result) = GetRequestSendId(context);
|
var (sendIdGuid, result) = GetRequestSendId(context);
|
||||||
if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid)
|
if (result != SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid)
|
||||||
{
|
{
|
||||||
context.Result = BuildErrorResult(result);
|
context.Result = BuildErrorResult(result);
|
||||||
return;
|
return;
|
||||||
@ -49,15 +47,10 @@ public class SendAccessGrantValidator(
|
|||||||
|
|
||||||
switch (method)
|
switch (method)
|
||||||
{
|
{
|
||||||
case NeverAuthenticate:
|
case NeverAuthenticate never:
|
||||||
// null send scenario.
|
// null send scenario.
|
||||||
// TODO PM-22675: Add send enumeration protection here (primarily benefits self hosted instances).
|
context.Result = await _sendNeverAuthenticateValidator.ValidateRequestAsync(context, never, sendIdGuid);
|
||||||
// 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);
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case NotAuthenticated:
|
case NotAuthenticated:
|
||||||
// automatically issue access token
|
// automatically issue access token
|
||||||
context.Result = BuildBaseSuccessResult(sendIdGuid);
|
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 the sendId is null then the request is the wrong shape and the request is invalid
|
||||||
if (sendId == null)
|
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
|
// the send_id is not null so the request is the correct shape, so we will attempt to parse it
|
||||||
try
|
try
|
||||||
@ -100,20 +93,20 @@ public class SendAccessGrantValidator(
|
|||||||
// Guid.Empty indicates an invalid send_id return invalid grant
|
// Guid.Empty indicates an invalid send_id return invalid grant
|
||||||
if (sendGuid == Guid.Empty)
|
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
|
catch
|
||||||
{
|
{
|
||||||
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds an error result for the specified error type.
|
/// Builds an error result for the specified error type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">This error is a constant string from <see cref="SendAccessConstants.GrantValidatorResults"/></param>
|
/// <param name="error">This error is a constant string from <see cref="SendAccessConstants.SendIdGuidValidatorResults"/></param>
|
||||||
/// <returns>The error result.</returns>
|
/// <returns>The error result.</returns>
|
||||||
private static GrantValidationResult BuildErrorResult(string error)
|
private static GrantValidationResult BuildErrorResult(string error)
|
||||||
{
|
{
|
||||||
@ -125,12 +118,12 @@ public class SendAccessGrantValidator(
|
|||||||
return error switch
|
return error switch
|
||||||
{
|
{
|
||||||
// Request is the wrong shape
|
// Request is the wrong shape
|
||||||
SendAccessConstants.GrantValidatorResults.SendIdRequired => new GrantValidationResult(
|
SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired => new GrantValidationResult(
|
||||||
TokenRequestErrors.InvalidRequest,
|
TokenRequestErrors.InvalidRequest,
|
||||||
errorDescription: _sendGrantValidatorErrorDescriptions[error],
|
errorDescription: _sendGrantValidatorErrorDescriptions[error],
|
||||||
customResponse),
|
customResponse),
|
||||||
// Request is correct shape but data is bad
|
// Request is correct shape but data is bad
|
||||||
SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult(
|
SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId => new GrantValidationResult(
|
||||||
TokenRequestErrors.InvalidGrant,
|
TokenRequestErrors.InvalidGrant,
|
||||||
errorDescription: _sendGrantValidatorErrorDescriptions[error],
|
errorDescription: _sendGrantValidatorErrorDescriptions[error],
|
||||||
customResponse),
|
customResponse),
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="globalSettings">We need access to a hash key to generate the error index.</param>
|
||||||
|
public class SendNeverAuthenticateRequestValidator(GlobalSettings globalSettings) : ISendAuthenticationMethodValidator<NeverAuthenticate>
|
||||||
|
{
|
||||||
|
private readonly string[] _errorOptions =
|
||||||
|
[
|
||||||
|
SendAccessConstants.EnumerationProtection.Guid,
|
||||||
|
SendAccessConstants.EnumerationProtection.Password,
|
||||||
|
SendAccessConstants.EnumerationProtection.Email
|
||||||
|
];
|
||||||
|
|
||||||
|
public Task<GrantValidationResult> 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<string, object>
|
||||||
|
{
|
||||||
|
{ 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
||||||
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
||||||
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
||||||
|
services.AddTransient<ISendAuthenticationMethodValidator<NeverAuthenticate>, SendNeverAuthenticateRequestValidator>();
|
||||||
|
|
||||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||||
var identityServerBuilder = services
|
var identityServerBuilder = services
|
||||||
|
|||||||
230
test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs
Normal file
230
test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs
Normal 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
|
||||||
|
}
|
||||||
@ -1,10 +1,8 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.IdentityServer;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
using Bit.Identity.IdentityServer.Enums;
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
@ -13,16 +11,14 @@ using Duende.IdentityServer.Validation;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
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
|
// 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.
|
// method throws as expected.
|
||||||
internal record AnUnknownAuthenticationMethod : SendAuthenticationMethod { }
|
internal record AnUnknownAuthenticationMethod : SendAuthenticationMethod { }
|
||||||
|
|
||||||
public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory factory) : IClassFixture<IdentityApplicationFactory>
|
public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly IdentityApplicationFactory _factory = factory;
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType()
|
public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType()
|
||||||
{
|
{
|
||||||
@ -39,7 +35,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -70,7 +66,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -125,7 +121,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -154,7 +150,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -183,7 +179,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var error = await client.PostAsync("/connect/token", requestBody);
|
var error = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -225,7 +221,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, "password123");
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, "password123");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -236,37 +232,4 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
Assert.Contains("access_token", content);
|
Assert.Contains("access_token", content);
|
||||||
Assert.Contains("Bearer", 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,16 @@
|
|||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.IdentityServer;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
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 Bit.IntegrationTestCommon.Factories;
|
||||||
using Duende.IdentityModel;
|
using Duende.IdentityModel;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
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]
|
[Fact]
|
||||||
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
|
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
|
||||||
{
|
{
|
||||||
@ -43,7 +31,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId); // No email
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No email
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -87,7 +75,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -130,7 +118,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: otp);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: otp);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -174,7 +162,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: invalidOtp);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: invalidOtp);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -216,7 +204,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -225,32 +213,4 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
var content = await response.Content.ReadAsStringAsync();
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,17 @@
|
|||||||
using Bit.Core.Auth.IdentityServer;
|
using Bit.Core.KeyManagement.Sends;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.KeyManagement.Sends;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Duende.IdentityModel;
|
using Duende.IdentityModel;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
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]
|
[Fact]
|
||||||
public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken()
|
public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken()
|
||||||
{
|
{
|
||||||
@ -54,7 +43,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, clientPasswordHash);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: clientPasswordHash);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -95,7 +84,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, wrongClientPasswordHash);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: wrongClientPasswordHash);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -131,7 +120,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId); // No password
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No password
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
@ -176,7 +165,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
|||||||
});
|
});
|
||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = CreateTokenRequestBody(sendId, string.Empty);
|
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, string.Empty);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.PostAsync("/connect/token", requestBody);
|
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(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||||
Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ using Bit.Test.Common.Helpers;
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
namespace Bit.Identity.IntegrationTest.RequestValidation.VaultAccess;
|
||||||
|
|
||||||
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
|
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
|
||||||
{
|
{
|
||||||
@ -1,12 +1,8 @@
|
|||||||
using System.Collections.Specialized;
|
using Bit.Core;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Auth.Identity;
|
using Bit.Core.Auth.Identity;
|
||||||
using Bit.Core.Auth.IdentityServer;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
using Bit.Identity.IdentityServer.Enums;
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -81,7 +77,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
var context = new ExtensionGrantValidationContext();
|
var context = new ExtensionGrantValidationContext();
|
||||||
|
|
||||||
tokenRequest.GrantType = CustomGrantTypes.SendAccess;
|
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
|
// To preserve the CreateTokenRequestBody method for more general usage we over write the sendId
|
||||||
tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format");
|
tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format");
|
||||||
@ -118,7 +114,9 @@ public class SendAccessGrantValidatorTests
|
|||||||
public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant(
|
public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant(
|
||||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
SutProvider<SendAccessGrantValidator> sutProvider,
|
SutProvider<SendAccessGrantValidator> sutProvider,
|
||||||
Guid sendId)
|
NeverAuthenticate neverAuthenticate,
|
||||||
|
Guid sendId,
|
||||||
|
GrantValidationResult expectedResult)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = SetupTokenRequest(
|
var context = SetupTokenRequest(
|
||||||
@ -128,14 +126,20 @@ public class SendAccessGrantValidatorTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ISendAuthenticationQuery>()
|
sutProvider.GetDependency<ISendAuthenticationQuery>()
|
||||||
.GetAuthenticationMethod(sendId)
|
.GetAuthenticationMethod(sendId)
|
||||||
.Returns(new NeverAuthenticate());
|
.Returns(neverAuthenticate);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendAuthenticationMethodValidator<NeverAuthenticate>>()
|
||||||
|
.ValidateRequestAsync(context, neverAuthenticate, sendId)
|
||||||
|
.Returns(expectedResult);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await sutProvider.Sut.ValidateAsync(context);
|
await sutProvider.Sut.ValidateAsync(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
Assert.Equal(expectedResult, context.Result);
|
||||||
Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription);
|
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<NeverAuthenticate>>()
|
||||||
|
.Received(1)
|
||||||
|
.ValidateRequestAsync(context, neverAuthenticate, sendId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -264,7 +268,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
public void GrantType_ReturnsCorrectType()
|
public void GrantType_ReturnsCorrectType()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
var validator = new SendAccessGrantValidator(null!, null!, null!, null!);
|
var validator = new SendAccessGrantValidator(null!, null!, null!, null!, null!);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType);
|
Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType);
|
||||||
@ -289,44 +293,9 @@ public class SendAccessGrantValidatorTests
|
|||||||
var context = new ExtensionGrantValidationContext();
|
var context = new ExtensionGrantValidationContext();
|
||||||
|
|
||||||
request.GrantType = CustomGrantTypes.SendAccess;
|
request.GrantType = CustomGrantTypes.SendAccess;
|
||||||
request.Raw = CreateTokenRequestBody(sendId);
|
request.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||||
context.Request = request;
|
context.Request = request;
|
||||||
|
|
||||||
return context;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,9 +31,9 @@ public class SendConstantsSnapshotTests
|
|||||||
public void GrantValidatorResults_Constants_HaveCorrectValues()
|
public void GrantValidatorResults_Constants_HaveCorrectValues()
|
||||||
{
|
{
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid);
|
Assert.Equal("valid_send_guid", SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid);
|
||||||
Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired);
|
Assert.Equal("send_id_required", SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired);
|
||||||
Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
Assert.Equal("send_id_invalid", SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@ -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.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.IdentityServer;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -28,7 +23,7 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
Guid sendId)
|
Guid sendId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
Request = tokenRequest
|
Request = tokenRequest
|
||||||
@ -61,8 +56,7 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
Guid sendId)
|
Guid sendId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
||||||
var emailOTP = new EmailOtp(["user@test.dev"]);
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
Request = tokenRequest
|
Request = tokenRequest
|
||||||
@ -96,7 +90,7 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
string generatedToken)
|
string generatedToken)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
Request = tokenRequest
|
Request = tokenRequest
|
||||||
@ -144,7 +138,7 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
string email)
|
string email)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
Request = tokenRequest
|
Request = tokenRequest
|
||||||
@ -179,7 +173,7 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
string otp)
|
string otp)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, otp);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, otp);
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
Request = tokenRequest
|
Request = tokenRequest
|
||||||
@ -231,7 +225,7 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
string invalidOtp)
|
string invalidOtp)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, invalidOtp);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, invalidOtp);
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
Request = tokenRequest
|
Request = tokenRequest
|
||||||
@ -278,33 +272,4 @@ public class SendEmailOtpRequestValidatorTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(validator);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,7 @@
|
|||||||
using System.Collections.Specialized;
|
using Bit.Core.Auth.Identity;
|
||||||
using Bit.Core.Auth.Identity;
|
|
||||||
using Bit.Core.Auth.IdentityServer;
|
|
||||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.KeyManagement.Sends;
|
using Bit.Core.KeyManagement.Sends;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -28,7 +23,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
Guid sendId)
|
Guid sendId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -58,7 +53,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
string clientPasswordHash)
|
string clientPasswordHash)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -92,7 +87,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
string clientPasswordHash)
|
string clientPasswordHash)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -130,7 +125,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
Guid sendId)
|
Guid sendId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: string.Empty);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -163,7 +158,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var whitespacePassword = " ";
|
var whitespacePassword = " ";
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: whitespacePassword);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -196,7 +191,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
var firstPassword = "first-password";
|
var firstPassword = "first-password";
|
||||||
var secondPassword = "second-password";
|
var secondPassword = "second-password";
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: [firstPassword, secondPassword]);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -229,7 +224,7 @@ public class SendPasswordRequestValidatorTests
|
|||||||
string clientPasswordHash)
|
string clientPasswordHash)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
|
||||||
|
|
||||||
var context = new ExtensionGrantValidationContext
|
var context = new ExtensionGrantValidationContext
|
||||||
{
|
{
|
||||||
@ -268,30 +263,4 @@ public class SendPasswordRequestValidatorTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(validator);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user