2025-09-02 16:48:57 -04:00
using System.Security.Claims ;
2025-10-22 15:13:31 -04:00
using Bit.Core ;
2025-09-04 10:08:03 -04:00
using Bit.Core.Auth.Identity ;
2025-09-02 16:48:57 -04:00
using Bit.Core.Auth.Identity.TokenProviders ;
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 (
2025-10-22 15:13:31 -04:00
IFeatureService featureService ,
2025-09-02 16:48:57 -04:00
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." } ,
2026-01-27 22:09:22 -05:00
{ SendAccessConstants . EmailOtpValidatorResults . EmailAndOtpRequired , $"{SendAccessConstants.TokenRequest.Email} and {SendAccessConstants.TokenRequest.Otp} are required." } ,
2025-09-02 16:48:57 -04:00
{ 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 ) ;
2026-01-27 22:50:09 -05:00
// It is an invalid request if the email is missing.
if ( string . IsNullOrEmpty ( email ) )
{
// Request is the wrong shape and doesn't contain an email field.'
return BuildErrorResult ( SendAccessConstants . EmailOtpValidatorResults . EmailRequired ) ;
}
2026-01-27 22:09:22 -05:00
/ *
2026-01-27 22:50:09 -05:00
* This is somewhat contradictory to our process where a poor shape means invalid_request and invalid
2026-01-27 22:09:22 -05:00
* data is invalid_grant .
2026-01-27 22:50:09 -05:00
* In this case the shape is correct and the data is invalid but to protect against enumeration we treat incorrect emails
* as invalid requests . The response for a request with a correct email which needs an OTP and a request
* that has an invalid email need to be the same otherwise an attacker could enumerate until a valid email is found .
2026-01-27 22:09:22 -05:00
* /
2026-01-27 22:50:09 -05:00
if ( ! authMethod . Emails . Contains ( email ) )
2025-09-02 16:48:57 -04:00
{
2026-01-27 22:09:22 -05:00
return BuildErrorResult ( SendAccessConstants . EmailOtpValidatorResults . EmailAndOtpRequired ) ;
2025-09-02 16:48:57 -04:00
}
// 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 ) ;
}
2025-10-22 15:13:31 -04:00
if ( featureService . IsEnabled ( FeatureFlagKeys . MJMLBasedEmailTemplates ) )
{
await mailService . SendSendEmailOtpEmailv2Async (
email ,
token ,
string . Format ( SendAccessConstants . OtpEmail . Subject , token ) ) ;
}
else
{
await mailService . SendSendEmailOtpEmailAsync (
email ,
token ,
string . Format ( SendAccessConstants . OtpEmail . Subject , token ) ) ;
}
2026-01-27 22:09:22 -05:00
return BuildErrorResult ( SendAccessConstants . EmailOtpValidatorResults . EmailAndOtpRequired ) ;
2025-09-02 16:48:57 -04:00
}
// 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 :
2026-01-27 22:09:22 -05:00
case SendAccessConstants . EmailOtpValidatorResults . EmailAndOtpRequired :
2025-09-02 16:48:57 -04:00
return new GrantValidationResult ( TokenRequestErrors . InvalidRequest ,
errorDescription : _sendEmailOtpValidatorErrorDescriptions [ error ] ,
new Dictionary < string , object >
{
{ SendAccessConstants . SendAccessError , error }
} ) ;
case SendAccessConstants . EmailOtpValidatorResults . EmailOtpInvalid :
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 ) ;
}
}