mirror of
https://github.com/bitwarden/server
synced 2025-12-15 15:53:59 +00:00
[PM-25050] limit failed 2fa emails to once per hour (#6227)
* limit failed 2fa emails to once per hour * Linting. --------- Co-authored-by: Todd Martin <tmartin@bitwarden.com>
This commit is contained in:
@@ -2,10 +2,13 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@@ -19,17 +22,93 @@ public class HandlebarsMailServiceTests
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IMailDeliveryService _mailDeliveryService;
|
||||
private readonly IMailEnqueuingService _mailEnqueuingService;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
|
||||
public HandlebarsMailServiceTests()
|
||||
{
|
||||
_globalSettings = new GlobalSettings();
|
||||
_mailDeliveryService = Substitute.For<IMailDeliveryService>();
|
||||
_mailEnqueuingService = Substitute.For<IMailEnqueuingService>();
|
||||
_distributedCache = Substitute.For<IDistributedCache>();
|
||||
|
||||
_sut = new HandlebarsMailService(
|
||||
_globalSettings,
|
||||
_mailDeliveryService,
|
||||
_mailEnqueuingService
|
||||
_mailEnqueuingService,
|
||||
_distributedCache
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFailedTwoFactorAttemptEmailAsync_FirstCall_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var failedType = TwoFactorProviderType.Email;
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var ip = "192.168.1.1";
|
||||
|
||||
_distributedCache.GetAsync(Arg.Any<string>()).Returns((byte[])null);
|
||||
|
||||
// Act
|
||||
await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
||||
await _distributedCache.Received(1).SetAsync(
|
||||
Arg.Is<string>(key => key == $"FailedTwoFactorAttemptEmail_{email}"),
|
||||
Arg.Any<byte[]>(),
|
||||
Arg.Any<DistributedCacheEntryOptions>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFailedTwoFactorAttemptEmailAsync_SecondCallWithinHour_DoesNotSendEmail()
|
||||
{
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var failedType = TwoFactorProviderType.Email;
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var ip = "192.168.1.1";
|
||||
|
||||
// Simulate cache hit (email was already sent)
|
||||
_distributedCache.GetAsync(Arg.Any<string>()).Returns([1]);
|
||||
|
||||
// Act
|
||||
await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.DidNotReceive().SendEmailAsync(Arg.Any<MailMessage>());
|
||||
await _distributedCache.DidNotReceive().SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFailedTwoFactorAttemptEmailAsync_DifferentEmails_SendsBothEmails()
|
||||
{
|
||||
// Arrange
|
||||
var email1 = "test1@example.com";
|
||||
var email2 = "test2@example.com";
|
||||
var failedType = TwoFactorProviderType.Email;
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var ip = "192.168.1.1";
|
||||
|
||||
_distributedCache.GetAsync(Arg.Any<string>()).Returns((byte[])null);
|
||||
|
||||
// Act
|
||||
await _sut.SendFailedTwoFactorAttemptEmailAsync(email1, failedType, utcNow, ip);
|
||||
await _sut.SendFailedTwoFactorAttemptEmailAsync(email2, failedType, utcNow, ip);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(2).SendEmailAsync(Arg.Any<MailMessage>());
|
||||
await _distributedCache.Received(1).SetAsync(
|
||||
Arg.Is<string>(key => key == $"FailedTwoFactorAttemptEmail_{email1}"),
|
||||
Arg.Any<byte[]>(),
|
||||
Arg.Any<DistributedCacheEntryOptions>()
|
||||
);
|
||||
await _distributedCache.Received(1).SetAsync(
|
||||
Arg.Is<string>(key => key == $"FailedTwoFactorAttemptEmail_{email2}"),
|
||||
Arg.Any<byte[]>(),
|
||||
Arg.Any<DistributedCacheEntryOptions>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,8 +216,9 @@ public class HandlebarsMailServiceTests
|
||||
};
|
||||
|
||||
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
|
||||
var distributedCache = Substitute.For<IDistributedCache>();
|
||||
|
||||
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService());
|
||||
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache);
|
||||
|
||||
var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync");
|
||||
|
||||
Reference in New Issue
Block a user