1
0
mirror of https://github.com/bitwarden/server synced 2026-01-03 00:53:37 +00:00

feat(2fa): [PM-24425] Add email on failed 2FA attempt

* Added email on failed 2FA attempt.

* Added tests.

* Adjusted email verbiage.

* Added feature flag.

* Undid accidental change.

* Undid unintentional change to clean up PR.

* Linting

* Added attempted method to email.

* Changes to email templates.

* Linting.

* Email format changes.

* Email formatting changes.
This commit is contained in:
Todd Martin
2025-08-11 16:39:43 -04:00
committed by GitHub
parent 5b67abba31
commit 3c5de319d1
13 changed files with 212 additions and 19 deletions

View File

@@ -1,11 +1,13 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Auth.Enums;
using Bit.Core.Models.Mail;
namespace Bit.Core.Auth.Models.Mail;
public class FailedAuthAttemptsModel : NewDeviceLoggedInModel
public class FailedAuthAttemptModel : NewDeviceLoggedInModel
{
public string AffectedEmail { get; set; }
public TwoFactorProviderType TwoFactorType { get; set; }
}

View File

@@ -127,6 +127,7 @@ public static class FeatureFlagKeys
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
public const string Otp6Digits = "pm-18612-otp-6-digits";
public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email";
/* Autofill Team */
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";

View File

@@ -0,0 +1,37 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 24px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold;" valign="top">
We've detected a failed login attempt
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
If you're having trouble with two-step login, please visit the <a target="_blank" clicktracking=off href="https://help.bitwarden.com" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">Help Center</a>.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
If you did not recently try to log in, open the <a target="_blank" clicktracking=off href="{{{WebVaultUrl}}}" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">web app</a> and take these immediate steps to secure your Bitwarden account:
<ul>
<li style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Deauthorize all devices</li>
<li style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Change your master password</li>
</ul>
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<hr />
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Account:</b> {{AffectedEmail}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Two-Step Login Method:</b> {{TwoFactorType}} <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP Address:</b> {{IpAddress}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@@ -0,0 +1,18 @@
{{#>BasicTextLayout}}
We've detected a failed login attempt
If you're having trouble with two-step login, please visit the Help Center (https://bitwarden.com/help/).
If you did not recently try to log in, open the web app ({{{WebVaultUrl}}}) and take these immediate steps to secure your Bitwarden account:
- Deauthorize all devices
- Change your master password
Account: {{AffectedEmail}}
Two-Step Login Method: {{TwoFactorType}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
{{/BasicTextLayout}}

View File

@@ -3,6 +3,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
@@ -29,6 +30,7 @@ public interface IMailService
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);

View File

@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Mail;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Mail;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Mail;
@@ -193,6 +194,25 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed two-step login attempt detected", email);
var model = new FailedAuthAttemptModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email,
TwoFactorType = failedType,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash
};
await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempt", model);
message.Category = "FailedTwoFactorAttempt";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)
{
var message = CreateDefaultMessage("Your Master Password Hint", email);

View File

@@ -3,6 +3,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
@@ -92,6 +93,11 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
public Task SendWelcomeEmailAsync(User user)
{
return Task.FromResult(0);

View File

@@ -36,6 +36,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IMailService _mailService;
protected ICurrentContext CurrentContext { get; }
protected IPolicyService PolicyService { get; }
@@ -61,7 +62,8 @@ public abstract class BaseRequestValidator<T> where T : class
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository
IAuthRequestRepository authRequestRepository,
IMailService mailService
)
{
_userManager = userManager;
@@ -80,6 +82,7 @@ public abstract class BaseRequestValidator<T> where T : class
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
PolicyRequirementQuery = policyRequirementQuery;
_authRequestRepository = authRequestRepository;
_mailService = mailService;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
@@ -160,6 +163,7 @@ public abstract class BaseRequestValidator<T> where T : class
}
else
{
await SendFailedTwoFactorEmail(user, twoFactorProviderType);
await UpdateFailedAuthDetailsAsync(user);
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
}
@@ -373,6 +377,14 @@ public abstract class BaseRequestValidator<T> where T : class
await _userRepository.ReplaceAsync(user);
}
private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType)
{
if (FeatureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail))
{
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow, CurrentContext.IpAddress);
}
}
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)
{
// Check current context/cache to see if user is in any organizations, avoids extra DB call if not

View File

@@ -46,7 +46,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IUpdateInstallationCommand updateInstallationCommand,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository)
IAuthRequestRepository authRequestRepository,
IMailService mailService)
: base(
userManager,
userService,
@@ -63,7 +64,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
ssoConfigRepository,
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository)
authRequestRepository,
mailService)
{
_userManager = userManager;
_updateInstallationCommand = updateInstallationCommand;

View File

@@ -40,7 +40,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IPolicyRequirementQuery policyRequirementQuery)
IPolicyRequirementQuery policyRequirementQuery,
IMailService mailService)
: base(
userManager,
userService,
@@ -57,7 +58,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
ssoConfigRepository,
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository)
authRequestRepository,
mailService)
{
_userManager = userManager;
_currentContext = currentContext;

View File

@@ -49,7 +49,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository)
IAuthRequestRepository authRequestRepository,
IMailService mailService)
: base(
userManager,
userService,
@@ -66,7 +67,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
ssoConfigRepository,
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository)
authRequestRepository,
mailService)
{
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;