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:
@@ -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.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)
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user