1
0
mirror of https://github.com/bitwarden/server synced 2025-12-14 15:23:42 +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:
Kyle Spearrin
2025-08-21 10:44:08 -07:00
committed by GitHub
parent 982aaf6f76
commit 1c98e59003
2 changed files with 105 additions and 3 deletions

View File

@@ -24,6 +24,7 @@ using Bit.Core.Utilities;
using Bit.Core.Vault.Models.Data;
using Core.Auth.Enums;
using HandlebarsDotNet;
using Microsoft.Extensions.Caching.Distributed;
namespace Bit.Core.Services;
@@ -31,10 +32,12 @@ public class HandlebarsMailService : IMailService
{
private const string Namespace = "Bit.Core.MailTemplates.Handlebars";
private const string _utcTimeZoneDisplay = "UTC";
private const string FailedTwoFactorAttemptCacheKeyFormat = "FailedTwoFactorAttemptEmail_{0}";
private readonly GlobalSettings _globalSettings;
private readonly IMailDeliveryService _mailDeliveryService;
private readonly IMailEnqueuingService _mailEnqueuingService;
private readonly IDistributedCache _distributedCache;
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();
private bool _registeredHelpersAndPartials = false;
@@ -42,11 +45,13 @@ public class HandlebarsMailService : IMailService
public HandlebarsMailService(
GlobalSettings globalSettings,
IMailDeliveryService mailDeliveryService,
IMailEnqueuingService mailEnqueuingService)
IMailEnqueuingService mailEnqueuingService,
IDistributedCache distributedCache)
{
_globalSettings = globalSettings;
_mailDeliveryService = mailDeliveryService;
_mailEnqueuingService = mailEnqueuingService;
_distributedCache = distributedCache;
}
public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token)
@@ -196,6 +201,16 @@ public class HandlebarsMailService : IMailService
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
// Check if we've sent this email within the last hour
var cacheKey = string.Format(FailedTwoFactorAttemptCacheKeyFormat, email);
var cachedValue = await _distributedCache.GetAsync(cacheKey);
if (cachedValue != null)
{
// Email was already sent within the last hour, skip sending
return;
}
var message = CreateDefaultMessage("Failed two-step login attempt detected", email);
var model = new FailedAuthAttemptModel()
{
@@ -211,6 +226,13 @@ public class HandlebarsMailService : IMailService
await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempt", model);
message.Category = "FailedTwoFactorAttempt";
await _mailDeliveryService.SendEmailAsync(message);
// Set cache entry with 1 hour expiration to prevent sending again
var cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
await _distributedCache.SetAsync(cacheKey, [1], cacheOptions);
}
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)