mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM- 22675] Send password auth method (#6228)
* feat: add Passwordvalidation * fix: update strings to constants * fix: add customResponse for rust consumption * test: add tests for SendPasswordValidator. fix: update tests for SendAccessGrantValidator * feat: update send access constants.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Core.Test")]
|
[assembly: InternalsVisibleTo("Core.Test")]
|
||||||
|
[assembly: InternalsVisibleTo("Identity.IntegrationTest")]
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// These control the results of the SendGrantValidator. <see cref="SendGrantValidator"/>
|
|
||||||
/// </summary>
|
|
||||||
internal enum SendGrantValidatorResultTypes
|
|
||||||
{
|
|
||||||
ValidSendGuid,
|
|
||||||
MissingSendId,
|
|
||||||
InvalidSendId
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// These control the results of the SendPasswordValidator. <see cref="SendPasswordRequestValidator"/>
|
|
||||||
/// </summary>
|
|
||||||
internal enum SendPasswordValidatorResultTypes
|
|
||||||
{
|
|
||||||
RequestPasswordDoesNotMatch
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
|
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// String constants for the Send Access user feature
|
||||||
|
/// </summary>
|
||||||
|
public static class SendAccessConstants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A catch all error type for send access related errors. Used mainly in the <see cref="GrantValidationResult.CustomResponse"/>
|
||||||
|
/// </summary>
|
||||||
|
public const string SendAccessError = "send_access_error_type";
|
||||||
|
public static class TokenRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// used to fetch Send from database.
|
||||||
|
/// </summary>
|
||||||
|
public const string SendId = "send_id";
|
||||||
|
/// <summary>
|
||||||
|
/// used to validate Send protected passwords
|
||||||
|
/// </summary>
|
||||||
|
public const string ClientB64HashedPassword = "password_hash_b64";
|
||||||
|
/// <summary>
|
||||||
|
/// email used to see if email is associated with the Send
|
||||||
|
/// </summary>
|
||||||
|
public const string Email = "email";
|
||||||
|
/// <summary>
|
||||||
|
/// Otp code sent to email associated with the Send
|
||||||
|
/// </summary>
|
||||||
|
public const string Otp = "otp";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GrantValidatorResults
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The sendId is valid and the request is well formed.
|
||||||
|
/// </summary>
|
||||||
|
public const string ValidSendGuid = "valid_send_guid";
|
||||||
|
/// <summary>
|
||||||
|
/// The sendId is missing from the request.
|
||||||
|
/// </summary>
|
||||||
|
public const string MissingSendId = "send_id_required";
|
||||||
|
/// <summary>
|
||||||
|
/// The sendId is invalid, does not match a known send.
|
||||||
|
/// </summary>
|
||||||
|
public const string InvalidSendId = "send_id_invalid";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PasswordValidatorResults
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The passwordHashB64 does not match the send's password hash.
|
||||||
|
/// </summary>
|
||||||
|
public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid";
|
||||||
|
/// <summary>
|
||||||
|
/// The passwordHashB64 is missing from the request.
|
||||||
|
/// </summary>
|
||||||
|
public const string RequestPasswordIsRequired = "password_hash_b64_required";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmailOtpValidatorResults
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the error code indicating that an email address is required.
|
||||||
|
/// </summary>
|
||||||
|
public const string EmailRequired = "email_required";
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the status indicating that both email and OTP are required, and the OTP has been sent.
|
||||||
|
/// </summary>
|
||||||
|
public const string EmailOtpSent = "email_and_otp_required_otp_sent";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ 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.Core.Utilities;
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
using Bit.Identity.IdentityServer.Enums;
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
|
|
||||||
using Duende.IdentityServer.Models;
|
using Duende.IdentityServer.Models;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
@@ -20,11 +19,11 @@ public class SendAccessGrantValidator(
|
|||||||
{
|
{
|
||||||
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
|
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
|
||||||
|
|
||||||
private static readonly Dictionary<SendGrantValidatorResultTypes, string>
|
private static readonly Dictionary<string, string>
|
||||||
_sendGrantValidatorErrors = new()
|
_sendGrantValidatorErrorDescriptions = new()
|
||||||
{
|
{
|
||||||
{ SendGrantValidatorResultTypes.MissingSendId, "send_id is required." },
|
{ SendAccessConstants.GrantValidatorResults.MissingSendId, $"{SendAccessConstants.TokenRequest.SendId} is required." },
|
||||||
{ SendGrantValidatorResultTypes.InvalidSendId, "send_id is invalid." }
|
{ SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ public class SendAccessGrantValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (sendIdGuid, result) = GetRequestSendId(context);
|
var (sendIdGuid, result) = GetRequestSendId(context);
|
||||||
if (result != SendGrantValidatorResultTypes.ValidSendGuid)
|
if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid)
|
||||||
{
|
{
|
||||||
context.Result = BuildErrorResult(result);
|
context.Result = BuildErrorResult(result);
|
||||||
return;
|
return;
|
||||||
@@ -55,7 +54,7 @@ public class SendAccessGrantValidator(
|
|||||||
// We should only map to password or email + OTP protected.
|
// 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 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.
|
// 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(SendGrantValidatorResultTypes.InvalidSendId);
|
context.Result = BuildErrorResult(SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case NotAuthenticated:
|
case NotAuthenticated:
|
||||||
@@ -64,7 +63,7 @@ public class SendAccessGrantValidator(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
case ResourcePassword rp:
|
case ResourcePassword rp:
|
||||||
// TODO PM-22675: Validate if the password is correct.
|
// Validate if the password is correct, or if we need to respond with a 400 stating a password has is required
|
||||||
context.Result = _sendPasswordRequestValidator.ValidateSendPassword(context, rp, sendIdGuid);
|
context.Result = _sendPasswordRequestValidator.ValidateSendPassword(context, rp, sendIdGuid);
|
||||||
return;
|
return;
|
||||||
case EmailOtp eo:
|
case EmailOtp eo:
|
||||||
@@ -84,15 +83,15 @@ public class SendAccessGrantValidator(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">request context</param>
|
/// <param name="context">request context</param>
|
||||||
/// <returns>a parsed sendId Guid and success result or a Guid.Empty and error type otherwise</returns>
|
/// <returns>a parsed sendId Guid and success result or a Guid.Empty and error type otherwise</returns>
|
||||||
private static (Guid, SendGrantValidatorResultTypes) GetRequestSendId(ExtensionGrantValidationContext context)
|
private static (Guid, string) GetRequestSendId(ExtensionGrantValidationContext context)
|
||||||
{
|
{
|
||||||
var request = context.Request.Raw;
|
var request = context.Request.Raw;
|
||||||
var sendId = request.Get("send_id");
|
var sendId = request.Get(SendAccessConstants.TokenRequest.SendId);
|
||||||
|
|
||||||
// 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, SendGrantValidatorResultTypes.MissingSendId);
|
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.MissingSendId);
|
||||||
}
|
}
|
||||||
// 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
|
||||||
@@ -102,13 +101,13 @@ 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, SendGrantValidatorResultTypes.InvalidSendId);
|
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
||||||
}
|
}
|
||||||
return (sendGuid, SendGrantValidatorResultTypes.ValidSendGuid);
|
return (sendGuid, SendAccessConstants.GrantValidatorResults.ValidSendGuid);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId);
|
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,18 +116,26 @@ public class SendAccessGrantValidator(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error type.</param>
|
/// <param name="error">The error type.</param>
|
||||||
/// <returns>The error result.</returns>
|
/// <returns>The error result.</returns>
|
||||||
private static GrantValidationResult BuildErrorResult(SendGrantValidatorResultTypes error)
|
private static GrantValidationResult BuildErrorResult(string error)
|
||||||
{
|
{
|
||||||
return error switch
|
return error switch
|
||||||
{
|
{
|
||||||
// Request is the wrong shape
|
// Request is the wrong shape
|
||||||
SendGrantValidatorResultTypes.MissingSendId => new GrantValidationResult(
|
SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult(
|
||||||
TokenRequestErrors.InvalidRequest,
|
TokenRequestErrors.InvalidRequest,
|
||||||
errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.MissingSendId]),
|
errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.MissingSendId],
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.MissingSendId}
|
||||||
|
}),
|
||||||
// Request is correct shape but data is bad
|
// Request is correct shape but data is bad
|
||||||
SendGrantValidatorResultTypes.InvalidSendId => new GrantValidationResult(
|
SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult(
|
||||||
TokenRequestErrors.InvalidGrant,
|
TokenRequestErrors.InvalidGrant,
|
||||||
errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.InvalidSendId]),
|
errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.InvalidSendId],
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.InvalidSendId }
|
||||||
|
}),
|
||||||
// should never get here
|
// should never get here
|
||||||
_ => new GrantValidationResult(TokenRequestErrors.InvalidRequest)
|
_ => new GrantValidationResult(TokenRequestErrors.InvalidRequest)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Bit.Core.Identity;
|
|||||||
using Bit.Core.KeyManagement.Sends;
|
using Bit.Core.KeyManagement.Sends;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Identity.IdentityServer.Enums;
|
using Bit.Identity.IdentityServer.Enums;
|
||||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
|
|
||||||
using Duende.IdentityServer.Models;
|
using Duende.IdentityServer.Models;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
@@ -16,31 +15,44 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// static object that contains the error messages for the SendPasswordRequestValidator.
|
/// static object that contains the error messages for the SendPasswordRequestValidator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Dictionary<SendPasswordValidatorResultTypes, string> _sendPasswordValidatorErrors = new()
|
private static readonly Dictionary<string, string> _sendPasswordValidatorErrorDescriptions = new()
|
||||||
{
|
{
|
||||||
{ SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch, "Request Password hash is invalid." }
|
{ SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid." },
|
||||||
|
{ SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required." }
|
||||||
};
|
};
|
||||||
|
|
||||||
public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId)
|
public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId)
|
||||||
{
|
{
|
||||||
var request = context.Request.Raw;
|
var request = context.Request.Raw;
|
||||||
var clientHashedPassword = request.Get("password_hash");
|
var clientHashedPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(clientHashedPassword))
|
// It is an invalid request _only_ if the passwordHashB64 is missing which indicated bad shape.
|
||||||
|
if (clientHashedPassword == null)
|
||||||
{
|
{
|
||||||
|
// Request is the wrong shape and doesn't contain a passwordHashB64 field.
|
||||||
return new GrantValidationResult(
|
return new GrantValidationResult(
|
||||||
TokenRequestErrors.InvalidRequest,
|
TokenRequestErrors.InvalidRequest,
|
||||||
errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]);
|
errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired],
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _sendPasswordHasher.PasswordHashMatches checks for an empty string so no need to do it before we make the call.
|
||||||
var hashMatches = _sendPasswordHasher.PasswordHashMatches(
|
var hashMatches = _sendPasswordHasher.PasswordHashMatches(
|
||||||
resourcePassword.Hash, clientHashedPassword);
|
resourcePassword.Hash, clientHashedPassword);
|
||||||
|
|
||||||
if (!hashMatches)
|
if (!hashMatches)
|
||||||
{
|
{
|
||||||
|
// Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty.
|
||||||
return new GrantValidationResult(
|
return new GrantValidationResult(
|
||||||
TokenRequestErrors.InvalidGrant,
|
TokenRequestErrors.InvalidGrant,
|
||||||
errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]);
|
errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch],
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return BuildSendPasswordSuccessResult(sendId);
|
return BuildSendPasswordSuccessResult(sendId);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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;
|
||||||
|
using Duende.IdentityModel;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -96,8 +97,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
}).CreateClient();
|
}).CreateClient();
|
||||||
|
|
||||||
var requestBody = new FormUrlEncodedContent([
|
var requestBody = new FormUrlEncodedContent([
|
||||||
new KeyValuePair<string, string>("grant_type", CustomGrantTypes.SendAccess),
|
new KeyValuePair<string, string>(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||||
new KeyValuePair<string, string>("client_id", BitwardenClient.Send)
|
new KeyValuePair<string, string>(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -105,8 +106,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
Assert.Contains("invalid_request", content);
|
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||||
Assert.Contains("send_id is required", content);
|
Assert.Contains($"{SendAccessConstants.TokenRequest.SendId} is required", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -245,16 +246,16 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
|||||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||||
var parameters = new List<KeyValuePair<string, string>>
|
var parameters = new List<KeyValuePair<string, string>>
|
||||||
{
|
{
|
||||||
new("grant_type", CustomGrantTypes.SendAccess),
|
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||||
new("client_id", BitwardenClient.Send ),
|
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ),
|
||||||
new("scope", ApiScopes.ApiSendAccess),
|
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||||
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
|
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
|
||||||
new("send_id", sendIdBase64)
|
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(password))
|
if (!string.IsNullOrEmpty(password))
|
||||||
{
|
{
|
||||||
parameters.Add(new("password_hash", password));
|
parameters.Add(new(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail))
|
if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail))
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.IdentityServer;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
|
||||||
|
{
|
||||||
|
private readonly IdentityApplicationFactory _factory;
|
||||||
|
|
||||||
|
public SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var passwordHash = "stored-password-hash";
|
||||||
|
var clientPasswordHash = "client-password-hash";
|
||||||
|
|
||||||
|
var client = _factory.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
// Enable feature flag
|
||||||
|
var featureService = Substitute.For<IFeatureService>();
|
||||||
|
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||||
|
services.AddSingleton(featureService);
|
||||||
|
|
||||||
|
// Mock send authentication query
|
||||||
|
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||||
|
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||||
|
.Returns(new ResourcePassword(passwordHash));
|
||||||
|
services.AddSingleton(sendAuthQuery);
|
||||||
|
|
||||||
|
// Mock password hasher to return true for matching passwords
|
||||||
|
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||||
|
passwordHasher.PasswordHashMatches(passwordHash, clientPasswordHash)
|
||||||
|
.Returns(true);
|
||||||
|
services.AddSingleton(passwordHasher);
|
||||||
|
});
|
||||||
|
}).CreateClient();
|
||||||
|
|
||||||
|
var requestBody = CreateTokenRequestBody(sendId, clientPasswordHash);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(response.IsSuccessStatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains(OidcConstants.TokenResponse.AccessToken, content);
|
||||||
|
Assert.Contains("bearer", content.ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendAccess_PasswordProtectedSend_InvalidPassword_ReturnsInvalidGrant()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var passwordHash = "stored-password-hash";
|
||||||
|
var wrongClientPasswordHash = "wrong-client-password-hash";
|
||||||
|
|
||||||
|
var client = _factory.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
var featureService = Substitute.For<IFeatureService>();
|
||||||
|
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||||
|
services.AddSingleton(featureService);
|
||||||
|
|
||||||
|
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||||
|
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||||
|
.Returns(new ResourcePassword(passwordHash));
|
||||||
|
services.AddSingleton(sendAuthQuery);
|
||||||
|
|
||||||
|
// Mock password hasher to return false for wrong passwords
|
||||||
|
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||||
|
passwordHasher.PasswordHashMatches(passwordHash, wrongClientPasswordHash)
|
||||||
|
.Returns(false);
|
||||||
|
services.AddSingleton(passwordHasher);
|
||||||
|
});
|
||||||
|
}).CreateClient();
|
||||||
|
|
||||||
|
var requestBody = CreateTokenRequestBody(sendId, wrongClientPasswordHash);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||||
|
Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendAccess_PasswordProtectedSend_MissingPassword_ReturnsInvalidRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var passwordHash = "stored-password-hash";
|
||||||
|
|
||||||
|
var client = _factory.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
var featureService = Substitute.For<IFeatureService>();
|
||||||
|
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||||
|
services.AddSingleton(featureService);
|
||||||
|
|
||||||
|
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||||
|
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||||
|
.Returns(new ResourcePassword(passwordHash));
|
||||||
|
services.AddSingleton(sendAuthQuery);
|
||||||
|
|
||||||
|
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||||
|
services.AddSingleton(passwordHasher);
|
||||||
|
});
|
||||||
|
}).CreateClient();
|
||||||
|
|
||||||
|
var requestBody = CreateTokenRequestBody(sendId); // No password
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||||
|
Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the password has is empty or whitespace it doesn't get passed to the server when the request is made.
|
||||||
|
/// This leads to an invalid request error since the absence of the password hash is considered a malformed request.
|
||||||
|
/// In the case that the passwordB64Hash _is_ empty or whitespace it would be an invalid grant since the request
|
||||||
|
/// has the correct shape.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task SendAccess_PasswordProtectedSend_EmptyPassword_ReturnsInvalidRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var passwordHash = "stored-password-hash";
|
||||||
|
|
||||||
|
var client = _factory.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
var featureService = Substitute.For<IFeatureService>();
|
||||||
|
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||||
|
services.AddSingleton(featureService);
|
||||||
|
|
||||||
|
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||||
|
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||||
|
.Returns(new ResourcePassword(passwordHash));
|
||||||
|
services.AddSingleton(sendAuthQuery);
|
||||||
|
|
||||||
|
// Mock password hasher to return false for empty passwords
|
||||||
|
var passwordHasher = Substitute.For<ISendPasswordHasher>();
|
||||||
|
passwordHasher.PasswordHashMatches(passwordHash, string.Empty)
|
||||||
|
.Returns(false);
|
||||||
|
services.AddSingleton(passwordHasher);
|
||||||
|
});
|
||||||
|
}).CreateClient();
|
||||||
|
|
||||||
|
var requestBody = CreateTokenRequestBody(sendId, string.Empty);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("/connect/token", requestBody);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||||
|
Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,7 +65,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, context.Result.Error);
|
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, context.Result.Error);
|
||||||
Assert.Equal("send_id is required.", context.Result.ErrorDescription);
|
Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is required.", context.Result.ErrorDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -84,7 +84,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty);
|
tokenRequest.Raw = CreateTokenRequestBody(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("send_id", "invalid-guid-format");
|
tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format");
|
||||||
context.Request = tokenRequest;
|
context.Request = tokenRequest;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -92,7 +92,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
||||||
Assert.Equal("send_id is invalid.", context.Result.ErrorDescription);
|
Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -111,7 +111,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
||||||
Assert.Equal("send_id is invalid.", context.Result.ErrorDescription);
|
Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -135,7 +135,7 @@ public class SendAccessGrantValidatorTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
||||||
Assert.Equal("send_id is invalid.", context.Result.ErrorDescription);
|
Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -297,37 +297,28 @@ public class SendAccessGrantValidatorTests
|
|||||||
|
|
||||||
var rawRequestParameters = new NameValueCollection
|
var rawRequestParameters = new NameValueCollection
|
||||||
{
|
{
|
||||||
{ "grant_type", CustomGrantTypes.SendAccess },
|
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||||
{ "client_id", BitwardenClient.Send },
|
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||||
{ "scope", ApiScopes.ApiSendAccess },
|
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||||
{ "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() },
|
{ "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||||
{ "send_id", sendIdBase64 }
|
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (passwordHash != null)
|
if (passwordHash != null)
|
||||||
{
|
{
|
||||||
rawRequestParameters.Add("password_hash", passwordHash);
|
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendEmail != null)
|
if (sendEmail != null)
|
||||||
{
|
{
|
||||||
rawRequestParameters.Add("send_email", sendEmail);
|
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otpCode != null && sendEmail != null)
|
if (otpCode != null && sendEmail != null)
|
||||||
{
|
{
|
||||||
rawRequestParameters.Add("otp_code", otpCode);
|
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawRequestParameters;
|
return rawRequestParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need a list of sendAuthentication methods to test against since we cannot create new objects in the BitAutoData
|
|
||||||
public static Dictionary<string, SendAuthenticationMethod> SendAuthenticationMethods => new()
|
|
||||||
{
|
|
||||||
{ "NeverAuthenticate", new NeverAuthenticate() }, // Send doesn't exist or is deleted
|
|
||||||
{ "NotAuthenticated", new NotAuthenticated() }, // Public send, no auth needed
|
|
||||||
// TODO: PM-22675 - {"ResourcePassword", new ResourcePassword("clientHashedPassword")}; // Password protected send
|
|
||||||
// TODO: PM-22678 - {"EmailOtp", new EmailOtp(["emailOtp@test.dev"]}; // Email + OTP protected send
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Identity;
|
||||||
|
using Bit.Core.IdentityServer;
|
||||||
|
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;
|
||||||
|
using Duende.IdentityModel;
|
||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Identity.Test.IdentityServer;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class SendPasswordRequestValidatorTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||||
|
Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required.", result.ErrorDescription);
|
||||||
|
|
||||||
|
// Verify password hasher was not called
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.PasswordHashMatches(Arg.Any<string>(), Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId,
|
||||||
|
string clientPasswordHash)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||||
|
Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid.", result.ErrorDescription);
|
||||||
|
|
||||||
|
// Verify password hasher was called with correct parameters
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.Received(1)
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_PasswordHashMatches_ReturnsSuccess(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId,
|
||||||
|
string clientPasswordHash)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsError);
|
||||||
|
|
||||||
|
var sub = result.Subject;
|
||||||
|
Assert.Equal(sendId, sub.GetSendId());
|
||||||
|
|
||||||
|
// Verify claims
|
||||||
|
Assert.Contains(sub.Claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString());
|
||||||
|
Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
||||||
|
|
||||||
|
// Verify password hasher was called
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.Received(1)
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, string.Empty)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||||
|
|
||||||
|
// Verify password hasher was called with empty string
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.Received(1)
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var whitespacePassword = " ";
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, whitespacePassword)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsError);
|
||||||
|
|
||||||
|
// Verify password hasher was called with whitespace string
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.Received(1)
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, whitespacePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var firstPassword = "first-password";
|
||||||
|
var secondPassword = "second-password";
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, firstPassword)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||||
|
|
||||||
|
// Verify password hasher was called with first value
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.Received(1)
|
||||||
|
.PasswordHashMatches(resourcePassword.Hash, $"{firstPassword},{secondPassword}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void ValidateSendPassword_SuccessResult_ContainsCorrectClaims(
|
||||||
|
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||||
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
ResourcePassword resourcePassword,
|
||||||
|
Guid sendId,
|
||||||
|
string clientPasswordHash)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||||
|
|
||||||
|
var context = new ExtensionGrantValidationContext
|
||||||
|
{
|
||||||
|
Request = tokenRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||||
|
.PasswordHashMatches(Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsError);
|
||||||
|
var sub = result.Subject;
|
||||||
|
|
||||||
|
var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendId);
|
||||||
|
Assert.NotNull(sendIdClaim);
|
||||||
|
Assert.Equal(sendId.ToString(), sendIdClaim.Value);
|
||||||
|
|
||||||
|
var typeClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.Type);
|
||||||
|
Assert.NotNull(typeClaim);
|
||||||
|
Assert.Equal(IdentityClientType.Send.ToString(), typeClaim.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WithValidParameters_CreatesInstance()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendPasswordHasher = Substitute.For<ISendPasswordHasher>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var validator = new SendPasswordRequestValidator(sendPasswordHasher);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user