mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
feat: Add MJML email templates for Send Email OTP feat: Implement MJML-based email templates for Send OTP functionality feat: Add feature flag support for Send Email OTP v2 emails feat: Update email view models and call sites for Send Email OTP fix: Modify the directory structure for MJML templates to have Auth directory for better team ownership fix: Rename `hero.js` to `mj-bw-hero.js` --- Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
277 lines
11 KiB
C#
277 lines
11 KiB
C#
using Bit.Core.Auth.Identity;
|
|
using Bit.Core.Auth.Identity.TokenProviders;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Tools.Models.Data;
|
|
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
|
using Bit.Test.Common.AutoFixture;
|
|
using Bit.Test.Common.AutoFixture.Attributes;
|
|
using Duende.IdentityModel;
|
|
using Duende.IdentityServer.Validation;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
|
|
|
[SutProviderCustomize]
|
|
public class SendEmailOtpRequestValidatorTests
|
|
{
|
|
[Theory, BitAutoData]
|
|
public async Task ValidateRequestAsync_MissingEmail_ReturnsInvalidRequest(
|
|
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
|
EmailOtp emailOtp,
|
|
Guid sendId)
|
|
{
|
|
// Arrange
|
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
|
var context = new ExtensionGrantValidationContext
|
|
{
|
|
Request = tokenRequest
|
|
};
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
|
|
|
// Assert
|
|
Assert.True(result.IsError);
|
|
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
|
Assert.Equal("email is required.", result.ErrorDescription);
|
|
|
|
// Verify no OTP generation or email sending occurred
|
|
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.DidNotReceive()
|
|
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.DidNotReceive()
|
|
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task ValidateRequestAsync_EmailNotInList_ReturnsInvalidRequest(
|
|
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
|
EmailOtp emailOtp,
|
|
string email,
|
|
Guid sendId)
|
|
{
|
|
// Arrange
|
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
|
var context = new ExtensionGrantValidationContext
|
|
{
|
|
Request = tokenRequest
|
|
};
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
|
|
|
// Assert
|
|
Assert.True(result.IsError);
|
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
|
Assert.Equal("email is invalid.", result.ErrorDescription);
|
|
|
|
// Verify no OTP generation or email sending occurred
|
|
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.DidNotReceive()
|
|
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.DidNotReceive()
|
|
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task ValidateRequestAsync_EmailWithoutOtp_GeneratesAndSendsOtp(
|
|
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
|
EmailOtp emailOtp,
|
|
Guid sendId,
|
|
string email,
|
|
string generatedToken)
|
|
{
|
|
// Arrange
|
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
|
var context = new ExtensionGrantValidationContext
|
|
{
|
|
Request = tokenRequest
|
|
};
|
|
|
|
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
|
|
|
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.GenerateTokenAsync(
|
|
SendAccessConstants.OtpToken.TokenProviderName,
|
|
SendAccessConstants.OtpToken.Purpose,
|
|
expectedUniqueId)
|
|
.Returns(generatedToken);
|
|
|
|
emailOtp = emailOtp with { Emails = [email] };
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
|
|
|
// Assert
|
|
Assert.True(result.IsError);
|
|
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
|
Assert.Equal("email otp sent.", result.ErrorDescription);
|
|
|
|
// Verify OTP generation
|
|
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.Received(1)
|
|
.GenerateTokenAsync(
|
|
SendAccessConstants.OtpToken.TokenProviderName,
|
|
SendAccessConstants.OtpToken.Purpose,
|
|
expectedUniqueId);
|
|
|
|
// Verify email sending
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.Received(1)
|
|
.SendSendEmailOtpEmailAsync(email, generatedToken, Arg.Any<string>());
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task ValidateRequestAsync_OtpGenerationFails_ReturnsGenerationFailedError(
|
|
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
|
EmailOtp emailOtp,
|
|
Guid sendId,
|
|
string email)
|
|
{
|
|
// Arrange
|
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
|
var context = new ExtensionGrantValidationContext
|
|
{
|
|
Request = tokenRequest
|
|
};
|
|
|
|
emailOtp = emailOtp with { Emails = [email] };
|
|
|
|
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
|
.Returns((string)null); // Generation fails
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
|
|
|
// Assert
|
|
Assert.True(result.IsError);
|
|
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
|
|
|
// Verify no email was sent
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.DidNotReceive()
|
|
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task ValidateRequestAsync_ValidOtp_ReturnsSuccess(
|
|
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
|
EmailOtp emailOtp,
|
|
Guid sendId,
|
|
string email,
|
|
string otp)
|
|
{
|
|
// Arrange
|
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, otp);
|
|
var context = new ExtensionGrantValidationContext
|
|
{
|
|
Request = tokenRequest
|
|
};
|
|
|
|
emailOtp = emailOtp with { Emails = [email] };
|
|
|
|
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
|
|
|
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.ValidateTokenAsync(
|
|
otp,
|
|
SendAccessConstants.OtpToken.TokenProviderName,
|
|
SendAccessConstants.OtpToken.Purpose,
|
|
expectedUniqueId)
|
|
.Returns(true);
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
|
|
|
// Assert
|
|
Assert.False(result.IsError);
|
|
var sub = result.Subject;
|
|
Assert.Equal(sendId.ToString(), sub.Claims.First(c => c.Type == Claims.SendAccessClaims.SendId).Value);
|
|
|
|
// Verify claims
|
|
Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString());
|
|
Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.Email && c.Value == email);
|
|
Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
|
|
|
// Verify OTP validation was called
|
|
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.Received(1)
|
|
.ValidateTokenAsync(otp, SendAccessConstants.OtpToken.TokenProviderName, SendAccessConstants.OtpToken.Purpose, expectedUniqueId);
|
|
|
|
// Verify no email was sent (validation only)
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.DidNotReceive()
|
|
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task ValidateRequestAsync_InvalidOtp_ReturnsInvalidGrant(
|
|
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
|
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
|
EmailOtp emailOtp,
|
|
Guid sendId,
|
|
string email,
|
|
string invalidOtp)
|
|
{
|
|
// Arrange
|
|
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, invalidOtp);
|
|
var context = new ExtensionGrantValidationContext
|
|
{
|
|
Request = tokenRequest
|
|
};
|
|
|
|
emailOtp = emailOtp with { Emails = [email] };
|
|
|
|
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
|
|
|
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.ValidateTokenAsync(invalidOtp,
|
|
SendAccessConstants.OtpToken.TokenProviderName,
|
|
SendAccessConstants.OtpToken.Purpose,
|
|
expectedUniqueId)
|
|
.Returns(false);
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
|
|
|
// Assert
|
|
Assert.True(result.IsError);
|
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
|
Assert.Equal("email otp is invalid.", result.ErrorDescription);
|
|
|
|
// Verify OTP validation was attempted
|
|
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
|
.Received(1)
|
|
.ValidateTokenAsync(invalidOtp,
|
|
SendAccessConstants.OtpToken.TokenProviderName,
|
|
SendAccessConstants.OtpToken.Purpose,
|
|
expectedUniqueId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithValidParameters_CreatesInstance()
|
|
{
|
|
// Arrange
|
|
var otpTokenProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
|
var mailService = Substitute.For<IMailService>();
|
|
var featureService = Substitute.For<IFeatureService>();
|
|
|
|
// Act
|
|
var validator = new SendEmailOtpRequestValidator(featureService, otpTokenProvider, mailService);
|
|
|
|
// Assert
|
|
Assert.NotNull(validator);
|
|
}
|
|
}
|