mirror of
https://github.com/bitwarden/server
synced 2025-12-11 13:53:40 +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.
(cherry picked from commit 3c5de319d1)
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Models.Mail;
|
using Bit.Core.Models.Mail;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Models.Mail;
|
namespace Bit.Core.Auth.Models.Mail;
|
||||||
|
|
||||||
public class FailedAuthAttemptsModel : NewDeviceLoggedInModel
|
public class FailedAuthAttemptModel : NewDeviceLoggedInModel
|
||||||
{
|
{
|
||||||
public string AffectedEmail { get; set; }
|
public string AffectedEmail { get; set; }
|
||||||
|
public TwoFactorProviderType TwoFactorType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -127,6 +127,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||||
public const string Otp6Digits = "pm-18612-otp-6-digits";
|
public const string Otp6Digits = "pm-18612-otp-6-digits";
|
||||||
|
public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email";
|
||||||
|
|
||||||
/* Autofill Team */
|
/* Autofill Team */
|
||||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||||
|
|||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
@@ -29,6 +30,7 @@ public interface IMailService
|
|||||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
|
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 SendNoMasterPasswordHintEmailAsync(string email);
|
||||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Mail;
|
using Bit.Core.AdminConsole.Models.Mail;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Mail;
|
using Bit.Core.Auth.Models.Mail;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models.Mail;
|
using Bit.Core.Billing.Models.Mail;
|
||||||
@@ -193,6 +194,25 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
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)
|
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("Your Master Password Hint", email);
|
var message = CreateDefaultMessage("Your Master Password Hint", email);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
@@ -92,6 +93,11 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
public Task SendWelcomeEmailAsync(User user)
|
public Task SendWelcomeEmailAsync(User user)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IAuthRequestRepository _authRequestRepository;
|
private readonly IAuthRequestRepository _authRequestRepository;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
|
||||||
protected ICurrentContext CurrentContext { get; }
|
protected ICurrentContext CurrentContext { get; }
|
||||||
protected IPolicyService PolicyService { get; }
|
protected IPolicyService PolicyService { get; }
|
||||||
@@ -61,7 +62,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository
|
IAuthRequestRepository authRequestRepository,
|
||||||
|
IMailService mailService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@@ -80,6 +82,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||||
PolicyRequirementQuery = policyRequirementQuery;
|
PolicyRequirementQuery = policyRequirementQuery;
|
||||||
_authRequestRepository = authRequestRepository;
|
_authRequestRepository = authRequestRepository;
|
||||||
|
_mailService = mailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||||
@@ -160,6 +163,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
await SendFailedTwoFactorEmail(user, twoFactorProviderType);
|
||||||
await UpdateFailedAuthDetailsAsync(user);
|
await UpdateFailedAuthDetailsAsync(user);
|
||||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, 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);
|
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)
|
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
|
// Check current context/cache to see if user is in any organizations, avoids extra DB call if not
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IUpdateInstallationCommand updateInstallationCommand,
|
IUpdateInstallationCommand updateInstallationCommand,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository)
|
IAuthRequestRepository authRequestRepository,
|
||||||
|
IMailService mailService)
|
||||||
: base(
|
: base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -63,7 +64,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
ssoConfigRepository,
|
ssoConfigRepository,
|
||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository)
|
authRequestRepository,
|
||||||
|
mailService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_updateInstallationCommand = updateInstallationCommand;
|
_updateInstallationCommand = updateInstallationCommand;
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IPolicyRequirementQuery policyRequirementQuery)
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
|
IMailService mailService)
|
||||||
: base(
|
: base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -57,7 +58,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
ssoConfigRepository,
|
ssoConfigRepository,
|
||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository)
|
authRequestRepository,
|
||||||
|
mailService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository)
|
IAuthRequestRepository authRequestRepository,
|
||||||
|
IMailService mailService)
|
||||||
: base(
|
: base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -66,7 +67,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
ssoConfigRepository,
|
ssoConfigRepository,
|
||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository)
|
authRequestRepository,
|
||||||
|
mailService)
|
||||||
{
|
{
|
||||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class BaseRequestValidatorTests
|
|||||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IAuthRequestRepository _authRequestRepository;
|
private readonly IAuthRequestRepository _authRequestRepository;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
|
||||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ public class BaseRequestValidatorTests
|
|||||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||||
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
|
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
|
||||||
|
_mailService = Substitute.For<IMailService>();
|
||||||
|
|
||||||
_sut = new BaseRequestValidatorTestWrapper(
|
_sut = new BaseRequestValidatorTestWrapper(
|
||||||
_userManager,
|
_userManager,
|
||||||
@@ -88,7 +90,8 @@ public class BaseRequestValidatorTests
|
|||||||
_ssoConfigRepository,
|
_ssoConfigRepository,
|
||||||
_userDecryptionOptionsBuilder,
|
_userDecryptionOptionsBuilder,
|
||||||
_policyRequirementQuery,
|
_policyRequirementQuery,
|
||||||
_authRequestRepository);
|
_authRequestRepository,
|
||||||
|
_mailService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logic path
|
/* Logic path
|
||||||
@@ -278,6 +281,98 @@ public class BaseRequestValidatorTests
|
|||||||
await _authRequestRepository.DidNotReceive().ReplaceAsync(Arg.Any<AuthRequest>());
|
await _authRequestRepository.DidNotReceive().ReplaceAsync(Arg.Any<AuthRequest>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ValidateAsync_TwoFactorTokenInvalid_ShouldSendFailedTwoFactorEmail(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
var user = requestContext.User;
|
||||||
|
|
||||||
|
// 1 -> initial validation passes
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
// 2 -> enable the FailedTwoFactorEmail feature flag
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true);
|
||||||
|
|
||||||
|
// 3 -> set up 2FA as required
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
// 4 -> provide invalid 2FA token
|
||||||
|
tokenRequest.Raw["TwoFactorToken"] = "invalid_token";
|
||||||
|
tokenRequest.Raw["TwoFactorProvider"] = TwoFactorProviderType.Email.ToString();
|
||||||
|
|
||||||
|
// 5 -> set up 2FA verification to fail
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.VerifyTwoFactorAsync(user, null, TwoFactorProviderType.Email, "invalid_token")
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Verify that the failed 2FA email was sent
|
||||||
|
await _mailService.Received(1)
|
||||||
|
.SendFailedTwoFactorAttemptEmailAsync(
|
||||||
|
user.Email,
|
||||||
|
TwoFactorProviderType.Email,
|
||||||
|
Arg.Any<DateTime>(),
|
||||||
|
Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ValidateAsync_TwoFactorRememberTokenExpired_ShouldNotSendFailedTwoFactorEmail(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
var user = requestContext.User;
|
||||||
|
|
||||||
|
// 1 -> initial validation passes
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
// 2 -> enable the FailedTwoFactorEmail feature flag
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true);
|
||||||
|
|
||||||
|
// 3 -> set up 2FA as required
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
// 4 -> provide invalid remember token (remember token expired)
|
||||||
|
tokenRequest.Raw["TwoFactorToken"] = "expired_remember_token";
|
||||||
|
tokenRequest.Raw["TwoFactorProvider"] = "5"; // Remember provider
|
||||||
|
|
||||||
|
// 5 -> set up remember token verification to fail
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.VerifyTwoFactorAsync(user, null, TwoFactorProviderType.Remember, "expired_remember_token")
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
// 6 -> set up dummy BuildTwoFactorResultAsync
|
||||||
|
var twoFactorResultDict = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "TwoFactorProviders", new[] { "0", "1" } },
|
||||||
|
{ "TwoFactorProviders2", new Dictionary<string, object>() }
|
||||||
|
};
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.BuildTwoFactorResultAsync(user, null)
|
||||||
|
.Returns(Task.FromResult(twoFactorResultDict));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Verify that the failed 2FA email was NOT sent for remember token expiration
|
||||||
|
await _mailService.DidNotReceive()
|
||||||
|
.SendFailedTwoFactorAttemptEmailAsync(Arg.Any<string>(), Arg.Any<TwoFactorProviderType>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
// Test grantTypes that require SSO when a user is in an organization that requires it
|
// Test grantTypes that require SSO when a user is in an organization that requires it
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData("password")]
|
[BitAutoData("password")]
|
||||||
@@ -600,12 +695,4 @@ public class BaseRequestValidatorTests
|
|||||||
Substitute.For<IServiceProvider>(),
|
Substitute.For<IServiceProvider>(),
|
||||||
Substitute.For<ILogger<UserManager<User>>>());
|
Substitute.For<ILogger<UserManager<User>>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddValidDeviceToRequest(ValidatedTokenRequest request)
|
|
||||||
{
|
|
||||||
request.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
|
||||||
request.Raw["DeviceType"] = "Android"; // must be valid device type
|
|
||||||
request.Raw["DeviceName"] = "DeviceName";
|
|
||||||
request.Raw["DevicePushToken"] = "DevicePushToken";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ IBaseRequestValidatorTestWrapper
|
|||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository) :
|
IAuthRequestRepository authRequestRepository,
|
||||||
|
IMailService mailService) :
|
||||||
base(
|
base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -80,7 +81,8 @@ IBaseRequestValidatorTestWrapper
|
|||||||
ssoConfigRepository,
|
ssoConfigRepository,
|
||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository)
|
authRequestRepository,
|
||||||
|
mailService)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user