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:
parent
7bdb07da93
commit
19d5817f8f
@ -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()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
7
src/Core/Models/Mail/FailedAuthAttemptsModel.cs
Normal file
7
src/Core/Models/Mail/FailedAuthAttemptsModel.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class FailedAuthAttemptsModel : NewDeviceLoggedInModel
|
||||
{
|
||||
public string AffectedEmail { get; set; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -16,6 +16,9 @@
|
||||
},
|
||||
"braintree": {
|
||||
"production": true
|
||||
},
|
||||
"captcha": {
|
||||
"maximumFailedLoginAttempts": 5
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
|
@ -13,6 +13,9 @@
|
||||
"internalApi": null,
|
||||
"internalVault": null,
|
||||
"internalSso": null
|
||||
},
|
||||
"captcha": {
|
||||
"maximumFailedLoginAttempts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
);
|
||||
|
||||
|
264
util/Migrator/DbScripts/2022-02-10_00_FailedLoginCaptcha.sql
Normal file
264
util/Migrator/DbScripts/2022-02-10_00_FailedLoginCaptcha.sql
Normal 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
|
||||
|
1528
util/MySqlMigrations/Migrations/20220301215315_FailedLoginCaptcha.Designer.cs
generated
Normal file
1528
util/MySqlMigrations/Migrations/20220301215315_FailedLoginCaptcha.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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)");
|
||||
|
@ -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;
|
1536
util/PostgresMigrations/Migrations/20220301211818_FailedLoginCaptcha.Designer.cs
generated
Normal file
1536
util/PostgresMigrations/Migrations/20220301211818_FailedLoginCaptcha.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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)");
|
||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user