mirror of
https://github.com/bitwarden/server
synced 2025-12-29 14:43:39 +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:
@@ -65,7 +65,7 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
// Assert
|
||||
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]
|
||||
@@ -84,7 +84,7 @@ public class SendAccessGrantValidatorTests
|
||||
tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty);
|
||||
|
||||
// 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;
|
||||
|
||||
// Act
|
||||
@@ -92,7 +92,7 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
// Assert
|
||||
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]
|
||||
@@ -111,7 +111,7 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
// Assert
|
||||
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]
|
||||
@@ -135,7 +135,7 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
// Assert
|
||||
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]
|
||||
@@ -297,37 +297,28 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ "grant_type", CustomGrantTypes.SendAccess },
|
||||
{ "client_id", BitwardenClient.Send },
|
||||
{ "scope", ApiScopes.ApiSendAccess },
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ "send_id", sendIdBase64 }
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (passwordHash != null)
|
||||
{
|
||||
rawRequestParameters.Add("password_hash", passwordHash);
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash);
|
||||
}
|
||||
|
||||
if (sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add("send_email", sendEmail);
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
|
||||
}
|
||||
|
||||
if (otpCode != null && sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add("otp_code", otpCode);
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
|
||||
}
|
||||
|
||||
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