mirror of
https://github.com/bitwarden/server
synced 2025-12-28 22:23:30 +00:00
[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:
@@ -0,0 +1,134 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
|
||||
public class SendEmailOtpRequestValidator(
|
||||
IOtpTokenProvider<DefaultOtpTokenProviderOptions> otpTokenProvider,
|
||||
IMailService mailService) : ISendAuthenticationMethodValidator<EmailOtp>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// static object that contains the error messages for the SendEmailOtpRequestValidator.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> _sendEmailOtpValidatorErrorDescriptions = new()
|
||||
{
|
||||
{ SendAccessConstants.EmailOtpValidatorResults.EmailRequired, $"{SendAccessConstants.TokenRequest.Email} is required." },
|
||||
{ SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent, "email otp sent." },
|
||||
{ SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, $"{SendAccessConstants.TokenRequest.Email} is invalid." },
|
||||
{ SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid, $"{SendAccessConstants.TokenRequest.Email} otp is invalid." },
|
||||
};
|
||||
|
||||
public async Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantValidationContext context, EmailOtp authMethod, Guid sendId)
|
||||
{
|
||||
var request = context.Request.Raw;
|
||||
// get email
|
||||
var email = request.Get(SendAccessConstants.TokenRequest.Email);
|
||||
|
||||
// It is an invalid request if the email is missing which indicated bad shape.
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
// Request is the wrong shape and doesn't contain an email field.
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
||||
}
|
||||
|
||||
// email must be in the list of emails in the EmailOtp array
|
||||
if (!authMethod.Emails.Contains(email))
|
||||
{
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
|
||||
}
|
||||
|
||||
// get otp from request
|
||||
var requestOtp = request.Get(SendAccessConstants.TokenRequest.Otp);
|
||||
var uniqueIdentifierForTokenCache = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
||||
if (string.IsNullOrEmpty(requestOtp))
|
||||
{
|
||||
// Since the request doesn't have an OTP, generate one
|
||||
var token = await otpTokenProvider.GenerateTokenAsync(
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
uniqueIdentifierForTokenCache);
|
||||
|
||||
// Verify that the OTP is generated
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
|
||||
}
|
||||
|
||||
await mailService.SendSendEmailOtpEmailAsync(
|
||||
email,
|
||||
token,
|
||||
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
|
||||
}
|
||||
|
||||
// validate request otp
|
||||
var otpResult = await otpTokenProvider.ValidateTokenAsync(
|
||||
requestOtp,
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
uniqueIdentifierForTokenCache);
|
||||
|
||||
// If OTP is invalid return error result
|
||||
if (!otpResult)
|
||||
{
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid);
|
||||
}
|
||||
|
||||
return BuildSuccessResult(sendId, email!);
|
||||
}
|
||||
|
||||
private static GrantValidationResult BuildErrorResult(string error)
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case SendAccessConstants.EmailOtpValidatorResults.EmailRequired:
|
||||
case SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent:
|
||||
return new GrantValidationResult(TokenRequestErrors.InvalidRequest,
|
||||
errorDescription: _sendEmailOtpValidatorErrorDescriptions[error],
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ SendAccessConstants.SendAccessError, error }
|
||||
});
|
||||
case SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid:
|
||||
case SendAccessConstants.EmailOtpValidatorResults.EmailInvalid:
|
||||
return new GrantValidationResult(
|
||||
TokenRequestErrors.InvalidGrant,
|
||||
errorDescription: _sendEmailOtpValidatorErrorDescriptions[error],
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ SendAccessConstants.SendAccessError, error }
|
||||
});
|
||||
default:
|
||||
return new GrantValidationResult(
|
||||
TokenRequestErrors.InvalidRequest,
|
||||
errorDescription: error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a successful validation result for the Send password send_access grant.
|
||||
/// </summary>
|
||||
/// <param name="sendId">Guid of the send being accessed.</param>
|
||||
/// <returns>successful grant validation result</returns>
|
||||
private static GrantValidationResult BuildSuccessResult(Guid sendId, string email)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.SendAccessClaims.SendId, sendId.ToString()),
|
||||
new(Claims.SendAccessClaims.Email, email),
|
||||
new(Claims.Type, IdentityClientType.Send.ToString())
|
||||
};
|
||||
|
||||
return new GrantValidationResult(
|
||||
subject: sendId.ToString(),
|
||||
authenticationMethod: CustomGrantTypes.SendAccess,
|
||||
claims: claims);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user