[PM-22678] Send email otp authentication method (#6255)

feat(auth): email OTP validation, and generalize authentication interface

- Generalized send authentication method interface
- Made validate method async
- Added email mail support for Handlebars
- Modified email templates to match future implementation

fix(auth): update constants, naming conventions, and error handling

- Renamed constants for clarity
- Updated claims naming convention
- Fixed error message generation
- Added customResponse for Rust consumption

test(auth): add and fix tests for validators and email

- Added tests for SendEmailOtpRequestValidator
- Updated tests for SendAccessGrantValidator

chore: apply dotnet formatting
This commit is contained in:
Ike
2025-09-02 16:48:57 -04:00
committed by GitHub
parent a5bed5dcaa
commit d2d3e0f11b
24 changed files with 1213 additions and 90 deletions

View File

@@ -8,7 +8,7 @@ using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher) : ISendPasswordRequestValidator
public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher) : ISendAuthenticationMethodValidator<ResourcePassword>
{
private readonly ISendPasswordHasher _sendPasswordHasher = sendPasswordHasher;
@@ -21,7 +21,7 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
{ SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required." }
};
public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId)
public Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId)
{
var request = context.Request.Raw;
var clientHashedPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword);
@@ -30,13 +30,13 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
if (clientHashedPassword == null)
{
// Request is the wrong shape and doesn't contain a passwordHashB64 field.
return new GrantValidationResult(
return Task.FromResult(new GrantValidationResult(
TokenRequestErrors.InvalidRequest,
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.
@@ -46,16 +46,16 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
if (!hashMatches)
{
// Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty.
return new GrantValidationResult(
return Task.FromResult(new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch],
new Dictionary<string, object>
{
{ SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch }
});
}));
}
return BuildSendPasswordSuccessResult(sendId);
return Task.FromResult(BuildSendPasswordSuccessResult(sendId));
}
/// <summary>
@@ -67,7 +67,7 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
{
var claims = new List<Claim>
{
new(Claims.SendId, sendId.ToString()),
new(Claims.SendAccessClaims.SendId, sendId.ToString()),
new(Claims.Type, IdentityClientType.Send.ToString())
};