Browse Source
* 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.pull/6243/head
10 changed files with 647 additions and 76 deletions
@ -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"; |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue