1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-22 02:51:33 +01:00

[Captcha] Implement failed logins ceiling (#1870)

* [Hacker1] Failed Login Attempts Captcha

* [Captcha] Implement failed logins ceiling

* Formatting

* Updated approach after implementation talks with Kyle

* Updated email templates // Updated calling arch for failed attempts

* Formatting

* Updated 2fa email links

* Renamed baserequest methods to better match their actions

* EF migrations/scripts

* Updated with requested changes

* Defaults for MaxiumumFailedLoginAttempts
This commit is contained in:
Vincent Salucci 2022-03-02 15:45:00 -06:00 committed by GitHub
parent 7bdb07da93
commit 19d5817f8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 3669 additions and 19 deletions

View File

@ -60,6 +60,8 @@ namespace Bit.Core.Entities
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; }
public int FailedLoginCount { get; set; }
public DateTime? LastFailedLoginDate { get; set; }
public void SetNewId()
{

View File

@ -39,6 +39,8 @@ namespace Bit.Core.IdentityServer
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IPolicyRepository _policyRepository;
private readonly IUserRepository _userRepository;
private readonly ICaptchaValidationService _captchaValidationService;
public BaseRequestValidator(
UserManager<User> userManager,
@ -54,7 +56,9 @@ namespace Bit.Core.IdentityServer
ILogger<ResourceOwnerPasswordValidator> logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
IPolicyRepository policyRepository)
IPolicyRepository policyRepository,
IUserRepository userRepository,
ICaptchaValidationService captchaValidationService)
{
_userManager = userManager;
_deviceRepository = deviceRepository;
@ -70,9 +74,11 @@ namespace Bit.Core.IdentityServer
_currentContext = currentContext;
_globalSettings = globalSettings;
_policyRepository = policyRepository;
_userRepository = userRepository;
_captchaValidationService = captchaValidationService;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request)
protected async Task ValidateAsync(T context, ValidatedTokenRequest request, bool unknownDevice = false)
{
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
@ -83,6 +89,7 @@ namespace Bit.Core.IdentityServer
var (user, valid) = await ValidateContextAsync(context);
if (!valid)
{
await UpdateFailedAuthDetailsAsync(user, false, unknownDevice);
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
return;
}
@ -102,6 +109,7 @@ namespace Bit.Core.IdentityServer
twoFactorProviderType, twoFactorToken);
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
{
await UpdateFailedAuthDetailsAsync(user, true, unknownDevice);
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
return;
}
@ -176,6 +184,7 @@ namespace Bit.Core.IdentityServer
customResponse.Add("TwoFactorToken", token);
}
await ResetFailedAuthDetailsAsync(user);
await SetSuccessResult(context, user, claims, customResponse);
}
@ -502,5 +511,38 @@ namespace Bit.Core.IdentityServer
return null;
}
private async Task ResetFailedAuthDetailsAsync(User user)
{
// Early escape if db hit not necessary
if (user.FailedLoginCount == 0)
{
return;
}
user.FailedLoginCount = 0;
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
{
var utcNow = DateTime.UtcNow;
user.FailedLoginCount = ++user.FailedLoginCount;
user.LastFailedLoginDate = user.RevisionDate = utcNow;
await _userRepository.ReplaceAsync(user);
if (_captchaValidationService.ValidateFailedAuthEmailConditions(unknownDevice, user.FailedLoginCount))
{
if (twoFactorInvalid)
{
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
}
else
{
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
}
}
}
}
}

View File

@ -41,10 +41,13 @@ namespace Bit.Core.IdentityServer
ICurrentContext currentContext,
GlobalSettings globalSettings,
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository)
ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository,
ICaptchaValidationService captchaValidationService)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository)
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, captchaValidationService)
{
_userManager = userManager;
_ssoConfigRepository = ssoConfigRepository;

View File

@ -37,10 +37,12 @@ namespace Bit.Core.IdentityServer
ICurrentContext currentContext,
GlobalSettings globalSettings,
IPolicyRepository policyRepository,
ICaptchaValidationService captchaValidationService)
ICaptchaValidationService captchaValidationService,
IUserRepository userRepository)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository)
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, captchaValidationService)
{
_userManager = userManager;
_userService = userService;
@ -60,7 +62,7 @@ namespace Bit.Core.IdentityServer
string bypassToken = null;
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
var unknownDevice = !await KnownDeviceAsync(user, context.Request);
if (unknownDevice && _captchaValidationService.RequireCaptchaValidation(_currentContext))
if (unknownDevice && _captchaValidationService.RequireCaptchaValidation(_currentContext, user.FailedLoginCount))
{
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
@ -83,7 +85,7 @@ namespace Bit.Core.IdentityServer
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
}
await ValidateAsync(context, context.Request);
await ValidateAsync(context, context.Request, unknownDevice);
if (context.Result.CustomResponse != null && bypassToken != null)
{
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;

View File

@ -0,0 +1,31 @@
{{#>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: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Additional security has been placed on your Bitwarden account.
</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 0 10px; -webkit-text-size-adjust: none;" valign="top">
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
</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 0 10px; -webkit-text-size-adjust: none;" valign="top">
<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;">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>
<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 0 10px; -webkit-text-size-adjust: none;" valign="top">
If this was you, you can remove the captcha requirement by successfully logging in.
</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; -webkit-text-size-adjust: none;" valign="top">
If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,13 @@
{{#>BasicTextLayout}}
Additional security has been placed on your Bitwarden account.
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
Account: {{AffectedEmail}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
If this was you, you can remove the captcha requirement by successfully logging in.
If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection.
{{/BasicTextLayout}}

View File

@ -0,0 +1,31 @@
{{#>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: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Additional security has been placed on your Bitwarden account.
</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 0 10px; -webkit-text-size-adjust: none;" valign="top">
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
</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 0 10px; -webkit-text-size-adjust: none;" valign="top">
<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;">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>
<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 0 10px; -webkit-text-size-adjust: none;" valign="top">
If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a <a target="_blank" clicktracking=off href="https://bitwarden.com/help/two-step-recovery-code/" 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;">recovery code</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 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; -webkit-text-size-adjust: none;" valign="top">
If this was not you, you should <a target="_blank" clicktracking=off href="https://bitwarden.com/help/master-password/#change-master-password" 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;">change your master password</a> immediately. You can view our tips for selecting a secure master password <a target="_blank" clicktracking=off href="https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/" 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;">here</a>.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,13 @@
{{#>BasicTextLayout}}
Additional security has been placed on your Bitwarden account.
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
Account: {{AffectedEmail}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code (https://bitwarden.com/help/two-step-recovery-code/).
If this was not you, you should change your master password (https://bitwarden.com/help/master-password/#change-master-password) immediately. You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/).
{{/BasicTextLayout}}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail
{
public class FailedAuthAttemptsModel : NewDeviceLoggedInModel
{
public string AffectedEmail { get; set; }
}
}

View File

@ -8,9 +8,10 @@ namespace Bit.Core.Services
{
string SiteKey { get; }
string SiteKeyResponseKeyName { get; }
bool RequireCaptchaValidation(ICurrentContext currentContext);
bool RequireCaptchaValidation(ICurrentContext currentContext, int? failedLoginCount = null);
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
string GenerateCaptchaBypassToken(User user);
bool ValidateCaptchaBypassToken(string encryptedToken, User user);
bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount);
}
}

View File

@ -53,5 +53,7 @@ namespace Bit.Core.Services
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName);
Task SendOTPEmailAsync(string email, string token);
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
}
}

View File

@ -83,8 +83,19 @@ namespace Bit.Core.Services
return root.GetProperty("success").GetBoolean();
}
public bool RequireCaptchaValidation(ICurrentContext currentContext) =>
currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
public bool RequireCaptchaValidation(ICurrentContext currentContext, int? failedLoginCount = null)
{
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts.GetValueOrDefault();
return currentContext.IsBot ||
_globalSettings.Captcha.ForceCaptchaRequired ||
failedLoginCeiling > 0 && failedLoginCount.GetValueOrDefault() >= failedLoginCeiling;
}
public bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount)
{
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts.GetValueOrDefault();
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
}
private static bool TokenIsApiKey(string bypassToken, User user) =>
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;

View File

@ -874,5 +874,39 @@ namespace Bit.Core.Services
message.Category = "OTP";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = "UTC",
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "FailedLoginAttempts", model);
message.Category = "FailedLoginAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = "UTC",
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "FailedTwoFactorAttempts", model);
message.Category = "FailedTwoFactorAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
}
}

View File

@ -8,11 +8,10 @@ namespace Bit.Core.Services
{
public string SiteKeyResponseKeyName => null;
public string SiteKey => null;
public bool RequireCaptchaValidation(ICurrentContext currentContext) => false;
public bool RequireCaptchaValidation(ICurrentContext currentContext, int? failedLoginCount) => false;
public bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount) => false;
public string GenerateCaptchaBypassToken(User user) => "";
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false;
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
{
return Task.FromResult(true);

View File

@ -220,5 +220,15 @@ namespace Bit.Core.Services
{
return Task.FromResult(0);
}
public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
}
}

View File

@ -463,6 +463,7 @@ namespace Bit.Core.Settings
public bool ForceCaptchaRequired { get; set; } = false;
public string HCaptchaSecretKey { get; set; }
public string HCaptchaSiteKey { get; set; }
public int? MaximumFailedLoginAttempts { get; set; }
}
public class StripeSettings

View File

@ -16,6 +16,9 @@
},
"braintree": {
"production": true
},
"captcha": {
"maximumFailedLoginAttempts": 5
}
},
"Logging": {

View File

@ -13,6 +13,9 @@
"internalApi": null,
"internalVault": null,
"internalSso": null
},
"captcha": {
"maximumFailedLoginAttempts": null
}
}
}

View File

@ -31,7 +31,9 @@
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesKeyConnector BIT = 0
@UsesKeyConnector BIT = 0,
@FailedLoginCount INT = 0,
@LastFailedLoginDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
@ -70,7 +72,9 @@ BEGIN
[RevisionDate],
[ApiKey],
[ForcePasswordReset],
[UsesKeyConnector]
[UsesKeyConnector],
[FailedLoginCount],
[LastFailedLoginDate]
)
VALUES
(
@ -106,6 +110,8 @@ BEGIN
@RevisionDate,
@ApiKey,
@ForcePasswordReset,
@UsesKeyConnector
@UsesKeyConnector,
@FailedLoginCount,
@LastFailedLoginDate
)
END

View File

@ -31,7 +31,9 @@
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesKeyConnector BIT = 0
@UsesKeyConnector BIT = 0,
@FailedLoginCount INT,
@LastFailedLoginDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
@ -70,7 +72,9 @@ BEGIN
[RevisionDate] = @RevisionDate,
[ApiKey] = @ApiKey,
[ForcePasswordReset] = @ForcePasswordReset,
[UsesKeyConnector] = @UsesKeyConnector
[UsesKeyConnector] = @UsesKeyConnector,
[FailedLoginCount] = @FailedLoginCount,
[LastFailedLoginDate] = @LastFailedLoginDate
WHERE
[Id] = @Id
END

View File

@ -32,6 +32,8 @@
[ApiKey] VARCHAR (30) NOT NULL,
[ForcePasswordReset] BIT NOT NULL,
[UsesKeyConnector] BIT NOT NULL,
[FailedLoginCount] INT NOT NULL,
[LastFailedLoginDate] DATETIME2 (7) NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@ -0,0 +1,264 @@
-- Table: User (FailedLoginCount)
IF COL_LENGTH('[dbo].[User]', 'FailedLoginCount') IS NULL
BEGIN
ALTER TABLE
[dbo].[User]
ADD
[FailedLoginCount] INT NULL
END
GO
UPDATE
[dbo].[User]
SET
[FailedLoginCount] = 0
WHERE
[FailedLoginCount] IS NULL
GO
ALTER TABLE
[dbo].[User]
ALTER COLUMN
[FailedLoginCount] INT NOT NULL
GO
-- Table: User (LastFailedLoginDate)
IF COL_LENGTH('[dbo].[User]', 'LastFailedLoginDate') IS NULL
BEGIN
ALTER TABLE
[dbo].[User]
ADD
[LastFailedLoginDate] DATETIME2(7) NULL
END
GO
-- View: User
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'UserView')
BEGIN
DROP VIEW [dbo].[UserView]
END
GO
CREATE VIEW [dbo].[UserView]
AS
SELECT
*
FROM
[dbo].[User]
GO
-- Stored Procedure: User_Create
IF OBJECT_ID('[dbo].[User_Create]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[User_Create]
END
GO
CREATE PROCEDURE [dbo].[User_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@Name NVARCHAR(50),
@Email NVARCHAR(256),
@EmailVerified BIT,
@MasterPassword NVARCHAR(300),
@MasterPasswordHint NVARCHAR(50),
@Culture NVARCHAR(10),
@SecurityStamp NVARCHAR(50),
@TwoFactorProviders NVARCHAR(MAX),
@TwoFactorRecoveryCode NVARCHAR(32),
@EquivalentDomains NVARCHAR(MAX),
@ExcludedGlobalEquivalentDomains NVARCHAR(MAX),
@AccountRevisionDate DATETIME2(7),
@Key NVARCHAR(MAX),
@PublicKey NVARCHAR(MAX),
@PrivateKey NVARCHAR(MAX),
@Premium BIT,
@PremiumExpirationDate DATETIME2(7),
@RenewalReminderDate DATETIME2(7),
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@ReferenceData VARCHAR(MAX),
@LicenseKey VARCHAR(100),
@Kdf TINYINT,
@KdfIterations INT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesKeyConnector BIT = 0,
@FailedLoginCount INT = 0,
@LastFailedLoginDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[User]
(
[Id],
[Name],
[Email],
[EmailVerified],
[MasterPassword],
[MasterPasswordHint],
[Culture],
[SecurityStamp],
[TwoFactorProviders],
[TwoFactorRecoveryCode],
[EquivalentDomains],
[ExcludedGlobalEquivalentDomains],
[AccountRevisionDate],
[Key],
[PublicKey],
[PrivateKey],
[Premium],
[PremiumExpirationDate],
[RenewalReminderDate],
[Storage],
[MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[ReferenceData],
[LicenseKey],
[Kdf],
[KdfIterations],
[CreationDate],
[RevisionDate],
[ApiKey],
[ForcePasswordReset],
[UsesKeyConnector],
[FailedLoginCount],
[LastFailedLoginDate]
)
VALUES
(
@Id,
@Name,
@Email,
@EmailVerified,
@MasterPassword,
@MasterPasswordHint,
@Culture,
@SecurityStamp,
@TwoFactorProviders,
@TwoFactorRecoveryCode,
@EquivalentDomains,
@ExcludedGlobalEquivalentDomains,
@AccountRevisionDate,
@Key,
@PublicKey,
@PrivateKey,
@Premium,
@PremiumExpirationDate,
@RenewalReminderDate,
@Storage,
@MaxStorageGb,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@ReferenceData,
@LicenseKey,
@Kdf,
@KdfIterations,
@CreationDate,
@RevisionDate,
@ApiKey,
@ForcePasswordReset,
@UsesKeyConnector,
@FailedLoginCount,
@LastFailedLoginDate
)
END
GO
-- Stored Procedure: User_Update
IF OBJECT_ID('[dbo].[User_Update]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[User_Update]
END
GO
CREATE PROCEDURE [dbo].[User_Update]
@Id UNIQUEIDENTIFIER,
@Name NVARCHAR(50),
@Email NVARCHAR(256),
@EmailVerified BIT,
@MasterPassword NVARCHAR(300),
@MasterPasswordHint NVARCHAR(50),
@Culture NVARCHAR(10),
@SecurityStamp NVARCHAR(50),
@TwoFactorProviders NVARCHAR(MAX),
@TwoFactorRecoveryCode NVARCHAR(32),
@EquivalentDomains NVARCHAR(MAX),
@ExcludedGlobalEquivalentDomains NVARCHAR(MAX),
@AccountRevisionDate DATETIME2(7),
@Key NVARCHAR(MAX),
@PublicKey NVARCHAR(MAX),
@PrivateKey NVARCHAR(MAX),
@Premium BIT,
@PremiumExpirationDate DATETIME2(7),
@RenewalReminderDate DATETIME2(7),
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@ReferenceData VARCHAR(MAX),
@LicenseKey VARCHAR(100),
@Kdf TINYINT,
@KdfIterations INT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesKeyConnector BIT = 0,
@FailedLoginCount INT,
@LastFailedLoginDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[User]
SET
[Name] = @Name,
[Email] = @Email,
[EmailVerified] = @EmailVerified,
[MasterPassword] = @MasterPassword,
[MasterPasswordHint] = @MasterPasswordHint,
[Culture] = @Culture,
[SecurityStamp] = @SecurityStamp,
[TwoFactorProviders] = @TwoFactorProviders,
[TwoFactorRecoveryCode] = @TwoFactorRecoveryCode,
[EquivalentDomains] = @EquivalentDomains,
[ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains,
[AccountRevisionDate] = @AccountRevisionDate,
[Key] = @Key,
[PublicKey] = @PublicKey,
[PrivateKey] = @PrivateKey,
[Premium] = @Premium,
[PremiumExpirationDate] = @PremiumExpirationDate,
[RenewalReminderDate] = @RenewalReminderDate,
[Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[ReferenceData] = @ReferenceData,
[LicenseKey] = @LicenseKey,
[Kdf] = @Kdf,
[KdfIterations] = @KdfIterations,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[ApiKey] = @ApiKey,
[ForcePasswordReset] = @ForcePasswordReset,
[UsesKeyConnector] = @UsesKeyConnector,
[FailedLoginCount] = @FailedLoginCount,
[LastFailedLoginDate] = @LastFailedLoginDate
WHERE
[Id] = @Id
END
GO

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Bit.MySqlMigrations.Migrations
{
public partial class FailedLoginCaptcha : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FailedLoginCount",
table: "User",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTime>(
name: "LastFailedLoginDate",
table: "User",
type: "datetime(6)",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FailedLoginCount",
table: "User");
migrationBuilder.DropColumn(
name: "LastFailedLoginDate",
table: "User");
}
}
}

View File

@ -1068,6 +1068,9 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<string>("ExcludedGlobalEquivalentDomains")
.HasColumnType("longtext");
b.Property<int>("FailedLoginCount")
.HasColumnType("int");
b.Property<bool>("ForcePasswordReset")
.HasColumnType("tinyint(1)");
@ -1091,6 +1094,9 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<string>("Key")
.HasColumnType("longtext");
b.Property<DateTime?>("LastFailedLoginDate")
.HasColumnType("datetime(6)");
b.Property<string>("LicenseKey")
.HasMaxLength(100)
.HasColumnType("varchar(100)");

View File

@ -0,0 +1,10 @@
START TRANSACTION;
ALTER TABLE `User` ADD `FailedLoginCount` int NOT NULL DEFAULT 0;
ALTER TABLE `User` ADD `LastFailedLoginDate` datetime(6) NULL;
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20220301215315_FailedLoginCaptcha', '5.0.12');
COMMIT;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Bit.PostgresMigrations.Migrations
{
public partial class FailedLoginCaptcha : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FailedLoginCount",
table: "User",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTime>(
name: "LastFailedLoginDate",
table: "User",
type: "timestamp without time zone",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FailedLoginCount",
table: "User");
migrationBuilder.DropColumn(
name: "LastFailedLoginDate",
table: "User");
}
}
}

View File

@ -1076,6 +1076,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<string>("ExcludedGlobalEquivalentDomains")
.HasColumnType("text");
b.Property<int>("FailedLoginCount")
.HasColumnType("integer");
b.Property<bool>("ForcePasswordReset")
.HasColumnType("boolean");
@ -1099,6 +1102,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<string>("Key")
.HasColumnType("text");
b.Property<DateTime?>("LastFailedLoginDate")
.HasColumnType("timestamp without time zone");
b.Property<string>("LicenseKey")
.HasMaxLength(100)
.HasColumnType("character varying(100)");

View File

@ -0,0 +1,10 @@
START TRANSACTION;
ALTER TABLE "User" ADD "FailedLoginCount" integer NOT NULL DEFAULT 0;
ALTER TABLE "User" ADD "LastFailedLoginDate" timestamp without time zone NULL;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20220301211818_FailedLoginCaptcha', '5.0.12');
COMMIT;