1
0
mirror of https://github.com/bitwarden/server synced 2025-12-29 22:54:00 +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:
Ike
2025-08-22 18:02:37 -04:00
committed by GitHub
parent 50b36bda2a
commit 3097e7f223
10 changed files with 647 additions and 76 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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";
}
}

View File

@@ -6,7 +6,6 @@ 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.Enums;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation;
@@ -20,11 +19,11 @@ public class SendAccessGrantValidator(
{
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
private static readonly Dictionary<SendGrantValidatorResultTypes, string>
_sendGrantValidatorErrors = new()
private static readonly Dictionary<string, string>
_sendGrantValidatorErrorDescriptions = new()
{
{ SendGrantValidatorResultTypes.MissingSendId, "send_id is required." },
{ SendGrantValidatorResultTypes.InvalidSendId, "send_id is invalid." }
{ SendAccessConstants.GrantValidatorResults.MissingSendId, $"{SendAccessConstants.TokenRequest.SendId} is required." },
{ SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
};
@@ -38,7 +37,7 @@ public class SendAccessGrantValidator(
}
var (sendIdGuid, result) = GetRequestSendId(context);
if (result != SendGrantValidatorResultTypes.ValidSendGuid)
if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid)
{
context.Result = BuildErrorResult(result);
return;
@@ -55,7 +54,7 @@ public class SendAccessGrantValidator(
// 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(SendGrantValidatorResultTypes.InvalidSendId);
context.Result = BuildErrorResult(SendAccessConstants.GrantValidatorResults.InvalidSendId);
return;
case NotAuthenticated:
@@ -64,7 +63,7 @@ public class SendAccessGrantValidator(
return;
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);
return;
case EmailOtp eo:
@@ -84,15 +83,15 @@ public class SendAccessGrantValidator(
/// </summary>
/// <param name="context">request context</param>
/// <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 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 (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
try
@@ -102,13 +101,13 @@ public class SendAccessGrantValidator(
// Guid.Empty indicates an invalid send_id return invalid grant
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
{
return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId);
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
}
}
@@ -117,18 +116,26 @@ public class SendAccessGrantValidator(
/// </summary>
/// <param name="error">The error type.</param>
/// <returns>The error result.</returns>
private static GrantValidationResult BuildErrorResult(SendGrantValidatorResultTypes error)
private static GrantValidationResult BuildErrorResult(string error)
{
return error switch
{
// Request is the wrong shape
SendGrantValidatorResultTypes.MissingSendId => new GrantValidationResult(
SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult(
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
SendGrantValidatorResultTypes.InvalidSendId => new GrantValidationResult(
SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.InvalidSendId]),
errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.InvalidSendId],
new Dictionary<string, object>
{
{ SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.InvalidSendId }
}),
// should never get here
_ => new GrantValidationResult(TokenRequestErrors.InvalidRequest)
};

View File

@@ -3,7 +3,6 @@ using Bit.Core.Identity;
using Bit.Core.KeyManagement.Sends;
using Bit.Core.Tools.Models.Data;
using Bit.Identity.IdentityServer.Enums;
using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation;
@@ -16,31 +15,44 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
/// <summary>
/// static object that contains the error messages for the SendPasswordRequestValidator.
/// </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)
{
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(
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(
resourcePassword.Hash, clientHashedPassword);
if (!hashMatches)
{
// Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty.
return new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]);
errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch],
new Dictionary<string, object>
{
{ SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch }
});
}
return BuildSendPasswordSuccessResult(sendId);