mirror of
https://github.com/bitwarden/server.git
synced 2025-05-22 12:04:27 -05: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 DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
public int FailedLoginCount { get; set; }
|
||||||
|
public DateTime? LastFailedLoginDate { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
|
@ -39,6 +39,8 @@ namespace Bit.Core.IdentityServer
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ICaptchaValidationService _captchaValidationService;
|
||||||
|
|
||||||
public BaseRequestValidator(
|
public BaseRequestValidator(
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
@ -54,7 +56,9 @@ namespace Bit.Core.IdentityServer
|
|||||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IPolicyRepository policyRepository)
|
IPolicyRepository policyRepository,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
ICaptchaValidationService captchaValidationService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
@ -70,9 +74,11 @@ namespace Bit.Core.IdentityServer
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_policyRepository = policyRepository;
|
_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 twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||||
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||||
@ -83,6 +89,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
var (user, valid) = await ValidateContextAsync(context);
|
var (user, valid) = await ValidateContextAsync(context);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
|
await UpdateFailedAuthDetailsAsync(user, false, unknownDevice);
|
||||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -102,6 +109,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
twoFactorProviderType, twoFactorToken);
|
twoFactorProviderType, twoFactorToken);
|
||||||
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
|
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||||
{
|
{
|
||||||
|
await UpdateFailedAuthDetailsAsync(user, true, unknownDevice);
|
||||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -176,6 +184,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
customResponse.Add("TwoFactorToken", token);
|
customResponse.Add("TwoFactorToken", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ResetFailedAuthDetailsAsync(user);
|
||||||
await SetSuccessResult(context, user, claims, customResponse);
|
await SetSuccessResult(context, user, claims, customResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,5 +511,38 @@ namespace Bit.Core.IdentityServer
|
|||||||
|
|
||||||
return null;
|
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,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
ISsoConfigRepository ssoConfigRepository)
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
ICaptchaValidationService captchaValidationService)
|
||||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||||
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository)
|
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
|
||||||
|
userRepository, captchaValidationService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
|
@ -37,10 +37,12 @@ namespace Bit.Core.IdentityServer
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
ICaptchaValidationService captchaValidationService)
|
ICaptchaValidationService captchaValidationService,
|
||||||
|
IUserRepository userRepository)
|
||||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||||
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository)
|
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
|
||||||
|
userRepository, captchaValidationService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
@ -60,7 +62,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
string bypassToken = null;
|
string bypassToken = null;
|
||||||
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
||||||
var unknownDevice = !await KnownDeviceAsync(user, context.Request);
|
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();
|
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
|
||||||
|
|
||||||
@ -83,7 +85,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ValidateAsync(context, context.Request);
|
await ValidateAsync(context, context.Request, unknownDevice);
|
||||||
if (context.Result.CustomResponse != null && bypassToken != null)
|
if (context.Result.CustomResponse != null && bypassToken != null)
|
||||||
{
|
{
|
||||||
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
|
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 SiteKey { get; }
|
||||||
string SiteKeyResponseKeyName { get; }
|
string SiteKeyResponseKeyName { get; }
|
||||||
bool RequireCaptchaValidation(ICurrentContext currentContext);
|
bool RequireCaptchaValidation(ICurrentContext currentContext, int? failedLoginCount = null);
|
||||||
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
|
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
|
||||||
string GenerateCaptchaBypassToken(User user);
|
string GenerateCaptchaBypassToken(User user);
|
||||||
bool ValidateCaptchaBypassToken(string encryptedToken, 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 SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
||||||
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName);
|
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName);
|
||||||
Task SendOTPEmailAsync(string email, string token);
|
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();
|
return root.GetProperty("success").GetBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RequireCaptchaValidation(ICurrentContext currentContext) =>
|
public bool RequireCaptchaValidation(ICurrentContext currentContext, int? failedLoginCount = null)
|
||||||
currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
|
{
|
||||||
|
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) =>
|
private static bool TokenIsApiKey(string bypassToken, User user) =>
|
||||||
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
|
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
|
||||||
|
@ -874,5 +874,39 @@ namespace Bit.Core.Services
|
|||||||
message.Category = "OTP";
|
message.Category = "OTP";
|
||||||
await _mailDeliveryService.SendEmailAsync(message);
|
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 SiteKeyResponseKeyName => null;
|
||||||
public string SiteKey => 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 string GenerateCaptchaBypassToken(User user) => "";
|
||||||
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false;
|
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false;
|
||||||
|
|
||||||
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
|
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
|
@ -220,5 +220,15 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
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 bool ForceCaptchaRequired { get; set; } = false;
|
||||||
public string HCaptchaSecretKey { get; set; }
|
public string HCaptchaSecretKey { get; set; }
|
||||||
public string HCaptchaSiteKey { get; set; }
|
public string HCaptchaSiteKey { get; set; }
|
||||||
|
public int? MaximumFailedLoginAttempts { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StripeSettings
|
public class StripeSettings
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
},
|
||||||
|
"captcha": {
|
||||||
|
"maximumFailedLoginAttempts": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null
|
||||||
|
},
|
||||||
|
"captcha": {
|
||||||
|
"maximumFailedLoginAttempts": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,9 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@ApiKey VARCHAR(30),
|
@ApiKey VARCHAR(30),
|
||||||
@ForcePasswordReset BIT = 0,
|
@ForcePasswordReset BIT = 0,
|
||||||
@UsesKeyConnector BIT = 0
|
@UsesKeyConnector BIT = 0,
|
||||||
|
@FailedLoginCount INT = 0,
|
||||||
|
@LastFailedLoginDate DATETIME2(7)
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -70,7 +72,9 @@ BEGIN
|
|||||||
[RevisionDate],
|
[RevisionDate],
|
||||||
[ApiKey],
|
[ApiKey],
|
||||||
[ForcePasswordReset],
|
[ForcePasswordReset],
|
||||||
[UsesKeyConnector]
|
[UsesKeyConnector],
|
||||||
|
[FailedLoginCount],
|
||||||
|
[LastFailedLoginDate]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -106,6 +110,8 @@ BEGIN
|
|||||||
@RevisionDate,
|
@RevisionDate,
|
||||||
@ApiKey,
|
@ApiKey,
|
||||||
@ForcePasswordReset,
|
@ForcePasswordReset,
|
||||||
@UsesKeyConnector
|
@UsesKeyConnector,
|
||||||
|
@FailedLoginCount,
|
||||||
|
@LastFailedLoginDate
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
|
@ -31,7 +31,9 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@ApiKey VARCHAR(30),
|
@ApiKey VARCHAR(30),
|
||||||
@ForcePasswordReset BIT = 0,
|
@ForcePasswordReset BIT = 0,
|
||||||
@UsesKeyConnector BIT = 0
|
@UsesKeyConnector BIT = 0,
|
||||||
|
@FailedLoginCount INT,
|
||||||
|
@LastFailedLoginDate DATETIME2(7)
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -70,7 +72,9 @@ BEGIN
|
|||||||
[RevisionDate] = @RevisionDate,
|
[RevisionDate] = @RevisionDate,
|
||||||
[ApiKey] = @ApiKey,
|
[ApiKey] = @ApiKey,
|
||||||
[ForcePasswordReset] = @ForcePasswordReset,
|
[ForcePasswordReset] = @ForcePasswordReset,
|
||||||
[UsesKeyConnector] = @UsesKeyConnector
|
[UsesKeyConnector] = @UsesKeyConnector,
|
||||||
|
[FailedLoginCount] = @FailedLoginCount,
|
||||||
|
[LastFailedLoginDate] = @LastFailedLoginDate
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
[ApiKey] VARCHAR (30) NOT NULL,
|
[ApiKey] VARCHAR (30) NOT NULL,
|
||||||
[ForcePasswordReset] BIT NOT NULL,
|
[ForcePasswordReset] BIT NOT NULL,
|
||||||
[UsesKeyConnector] BIT NOT NULL,
|
[UsesKeyConnector] BIT NOT NULL,
|
||||||
|
[FailedLoginCount] INT NOT NULL,
|
||||||
|
[LastFailedLoginDate] DATETIME2 (7) NULL,
|
||||||
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
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")
|
b.Property<string>("ExcludedGlobalEquivalentDomains")
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("FailedLoginCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<bool>("ForcePasswordReset")
|
b.Property<bool>("ForcePasswordReset")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
@ -1091,6 +1094,9 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastFailedLoginDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<string>("LicenseKey")
|
b.Property<string>("LicenseKey")
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("varchar(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")
|
b.Property<string>("ExcludedGlobalEquivalentDomains")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("FailedLoginCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<bool>("ForcePasswordReset")
|
b.Property<bool>("ForcePasswordReset")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
@ -1099,6 +1102,9 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastFailedLoginDate")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
b.Property<string>("LicenseKey")
|
b.Property<string>("LicenseKey")
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("character varying(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…
x
Reference in New Issue
Block a user