Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,9 @@ public static class EmailOtpValidatorResults
/// </summary>
public const string EmailRequired = "email_required";
/// <summary>
/// Represents the error code indicating that an email address is invalid.
/// </summary>
public const string EmailInvalid = "email_invalid";
/// <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";
public const string EmailAndOtpRequired = "email_and_otp_required";
/// <summary>
/// Represents the status indicating that both email and OTP are required, and the OTP is invalid.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public class SendEmailOtpRequestValidator(
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.EmailAndOtpRequired, $"{SendAccessConstants.TokenRequest.Email} and {SendAccessConstants.TokenRequest.Otp} are required." },
{ SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid, $"{SendAccessConstants.TokenRequest.Email} otp is invalid." },
};

Expand All @@ -33,7 +32,7 @@ public async Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantVali
// get email
var email = request.Get(SendAccessConstants.TokenRequest.Email);

// It is an invalid request if the email is missing which indicated bad shape.
// 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.
Expand All @@ -43,9 +42,16 @@ public async Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantVali
// email hash must be in the list of email hashes in the EmailOtp array
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(email));
string hashEmailHex = Convert.ToHexString(hashBytes).ToUpperInvariant();
/*
* This is somewhat contradictory to our process where a poor shape means invalid_request and invalid
* data is invalid_grant.
* 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.
*/
if (!authMethod.EmailHashes.Contains(hashEmailHex))
{
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired);
}

// get otp from request
Expand All @@ -70,7 +76,7 @@ await mailService.SendSendEmailOtpEmailAsync(
token,
string.Format(SendAccessConstants.OtpEmail.Subject, token));

return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired);
}

// validate request otp
Expand All @@ -94,15 +100,14 @@ private static GrantValidationResult BuildErrorResult(string error)
switch (error)
{
case SendAccessConstants.EmailOtpValidatorResults.EmailRequired:
case SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent:
case SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired:
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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public Task<GrantValidationResult> ValidateRequestAsync(
break;
case SendAccessConstants.EnumerationProtection.Email:
var hasEmail = request.Get(SendAccessConstants.TokenRequest.Email) is not null;
errorType = hasEmail ? SendAccessConstants.EmailOtpValidatorResults.EmailInvalid
errorType = hasEmail ? SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired
: SendAccessConstants.EmailOtpValidatorResults.EmailRequired;
break;
case SendAccessConstants.EnumerationProtection.Password:
Expand All @@ -64,7 +64,7 @@ private static GrantValidationResult BuildErrorResult(string errorType)
SendAccessConstants.EnumerationProtection.Guid => TokenRequestErrors.InvalidGrant,
SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired => TokenRequestErrors.InvalidGrant,
SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch => TokenRequestErrors.InvalidRequest,
SendAccessConstants.EmailOtpValidatorResults.EmailInvalid => TokenRequestErrors.InvalidGrant,
SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired => TokenRequestErrors.InvalidRequest,
SendAccessConstants.EmailOtpValidatorResults.EmailRequired => TokenRequestErrors.InvalidRequest,
_ => TokenRequestErrors.InvalidGrant
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail
// Assert
var content = await response.Content.ReadAsStringAsync();
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
Assert.Contains("email otp sent", content);
Assert.Contains("email and otp are required", content);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public async Task SendAccess_NeverAuthenticateSend_ReturnsEmailRequired()
}

[Fact]
public async Task SendAccess_NeverAuthenticateSend_WithEmail_ReturnsEmailInvalid()
public async Task SendAccess_NeverAuthenticateSend_WithEmail_ReturnsEmailAndOtpRequired()
{
// Arrange
var email = "[email protected]";
Expand All @@ -77,10 +77,10 @@ public async Task SendAccess_NeverAuthenticateSend_WithEmail_ReturnsEmailInvalid
var content = await response.Content.ReadAsStringAsync();

// should be invalid grant
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);

// Try to compel the invalid email error
var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailInvalid;
var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired;
Assert.Contains(expectedError, content);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ public void PasswordValidatorResults_Constants_HaveCorrectValues()
public void EmailOtpValidatorResults_Constants_HaveCorrectValues()
{
// Assert
Assert.Equal("email_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
Assert.Equal("email_required", SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
Assert.Equal("email_and_otp_required_otp_sent", SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
Assert.Equal("email_and_otp_required", SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired);
Assert.Equal("otp_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid);
Assert.Equal("otp_generation_failed", SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public async Task ValidateRequestAsync_EmailNotInList_ReturnsInvalidRequest(

// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Equal("email is invalid.", result.ErrorDescription);
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
Assert.Equal("email and otp are required.", result.ErrorDescription);

// Verify no OTP generation or email sending occurred
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
Expand Down Expand Up @@ -115,7 +115,7 @@ public async Task ValidateRequestAsync_EmailWithoutOtp_GeneratesAndSendsOtp(
// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
Assert.Equal("email otp sent.", result.ErrorDescription);
Assert.Equal("email and otp are required.", result.ErrorDescription);

// Verify OTP generation
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public async Task ValidateRequestAsync_GuidErrorSelected_ReturnsInvalidSendId(
}

[Theory, BitAutoData]
public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailInvalid(
public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailAndOtpRequired(
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
string email)
Expand All @@ -69,12 +69,12 @@ public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailI

// Assert
Assert.True(result.IsError);
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, result.ErrorDescription);
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired, result.ErrorDescription);

var customResponse = result.CustomResponse as Dictionary<string, object>;
Assert.NotNull(customResponse);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, customResponse[SendAccessConstants.SendAccessError]);
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired, customResponse[SendAccessConstants.SendAccessError]);
}

[Theory, BitAutoData]
Expand Down
Loading