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
4 changes: 2 additions & 2 deletions src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public record ResourcePassword(string Hash) : SendAuthenticationMethod;
/// <summary>
/// Create a send claim by requesting a one time password (OTP) confirmation code.
/// </summary>
/// <param name="Emails">
/// <param name="EmailHashes">
/// The list of email address **hashes** permitted access to the send.
/// </param>
public record EmailOtp(string[] Emails) : SendAuthenticationMethod;
public record EmailOtp(string[] EmailHashes) : SendAuthenticationMethod;
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
๏ปฟusing System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Bit.Core;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
Expand Down Expand Up @@ -40,8 +42,10 @@ public async Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantVali
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
}

// email must be in the list of emails in the EmailOtp array
if (!authMethod.Emails.Contains(email))
// 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();
if (!authMethod.EmailHashes.Contains(hashEmailHex))
{
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
}
Expand Down
17 changes: 17 additions & 0 deletions test/Common/Helpers/CryptographyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
๏ปฟusing System.Security.Cryptography;
using System.Text;

namespace Bit.Test.Common.Helpers;

public class CryptographyHelper
{
/// <summary>
/// Returns a hex-encoded, SHA256 hash for the given string
/// </summary>
public static string HashAndEncode(string text)
{
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
var hashEncoded = Convert.ToHexString(hashBytes).ToUpperInvariant();
return hashEncoded;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public async Task GetAuthenticationMethod_WithEmailHashes_ParsesEmailHashesCorre

// Assert
var emailOtp = Assert.IsType<EmailOtp>(result);
Assert.Equal(expectedEmailHashes, emailOtp.Emails);
Assert.Equal(expectedEmailHashes, emailOtp.EmailHashes);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.Helpers;
using Duende.IdentityModel;
using NSubstitute;
using Xunit;
Expand Down Expand Up @@ -60,7 +61,7 @@ public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail

var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
sendAuthQuery.GetAuthenticationMethod(sendId)
.Returns(new EmailOtp([email]));
.Returns(new EmailOtp([CryptographyHelper.HashAndEncode(email)]));
services.AddSingleton(sendAuthQuery);

// Mock OTP token provider
Expand All @@ -75,6 +76,7 @@ public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail
});
}).CreateClient();


var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP

// Act
Expand Down Expand Up @@ -104,7 +106,7 @@ public async Task SendAccess_EmailOtpProtectedSend_ValidOtp_ReturnsAccessToken()

var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
sendAuthQuery.GetAuthenticationMethod(sendId)
.Returns(new EmailOtp(new[] { email }));
.Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) }));
services.AddSingleton(sendAuthQuery);

// Mock OTP token provider to validate successfully
Expand Down Expand Up @@ -148,7 +150,7 @@ public async Task SendAccess_EmailOtpProtectedSend_InvalidOtp_ReturnsInvalidGran

var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
sendAuthQuery.GetAuthenticationMethod(sendId)
.Returns(new EmailOtp(new[] { email }));
.Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) }));
services.AddSingleton(sendAuthQuery);

// Mock OTP token provider to validate as false
Expand Down Expand Up @@ -190,7 +192,7 @@ public async Task SendAccess_EmailOtpProtectedSend_OtpGenerationFails_ReturnsInv

var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
sendAuthQuery.GetAuthenticationMethod(sendId)
.Returns(new EmailOtp(new[] { email }));
.Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) }));
services.AddSingleton(sendAuthQuery);

// Mock OTP token provider to fail generation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Duende.IdentityModel;
using Duende.IdentityServer.Validation;
using NSubstitute;
Expand Down Expand Up @@ -105,7 +106,8 @@ public async Task ValidateRequestAsync_EmailWithoutOtp_GeneratesAndSendsOtp(
expectedUniqueId)
.Returns(generatedToken);

emailOtp = emailOtp with { Emails = [email] };
var emailHash = CryptographyHelper.HashAndEncode(email);
emailOtp = emailOtp with { EmailHashes = [emailHash] };

// Act
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
Expand Down Expand Up @@ -144,7 +146,8 @@ public async Task ValidateRequestAsync_OtpGenerationFails_ReturnsGenerationFaile
Request = tokenRequest
};

emailOtp = emailOtp with { Emails = [email] };
var emailHash = CryptographyHelper.HashAndEncode(email);
emailOtp = emailOtp with { EmailHashes = [emailHash] };

sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
Expand Down Expand Up @@ -179,7 +182,8 @@ public async Task ValidateRequestAsync_ValidOtp_ReturnsSuccess(
Request = tokenRequest
};

emailOtp = emailOtp with { Emails = [email] };
var emailHash = CryptographyHelper.HashAndEncode(email);
emailOtp = emailOtp with { EmailHashes = [emailHash] };

var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);

Expand Down Expand Up @@ -231,7 +235,8 @@ public async Task ValidateRequestAsync_InvalidOtp_ReturnsInvalidGrant(
Request = tokenRequest
};

emailOtp = emailOtp with { Emails = [email] };
var emailHash = CryptographyHelper.HashAndEncode(email);
emailOtp = emailOtp with { EmailHashes = [emailHash] };

var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);

Expand Down
Loading