mirror of
https://github.com/bitwarden/server
synced 2025-12-10 13:23:27 +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
|
||||
#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; }
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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.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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,6 +50,7 @@ public class BaseRequestValidatorTests
|
||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IMailService _mailService;
|
||||
|
||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||
|
||||
@@ -71,6 +72,7 @@ public class BaseRequestValidatorTests
|
||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
@@ -88,7 +90,8 @@ public class BaseRequestValidatorTests
|
||||
_ssoConfigRepository,
|
||||
_userDecryptionOptionsBuilder,
|
||||
_policyRequirementQuery,
|
||||
_authRequestRepository);
|
||||
_authRequestRepository,
|
||||
_mailService);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
@@ -278,6 +281,98 @@ public class BaseRequestValidatorTests
|
||||
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
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
@@ -600,12 +695,4 @@ public class BaseRequestValidatorTests
|
||||
Substitute.For<IServiceProvider>(),
|
||||
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,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository) :
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService) :
|
||||
base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -80,7 +81,8 @@ IBaseRequestValidatorTestWrapper
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery,
|
||||
authRequestRepository)
|
||||
authRequestRepository,
|
||||
mailService)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user