1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 17:12:49 -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:
Vincent Salucci
2022-03-02 15:45:00 -06:00
committed by GitHub
parent 7bdb07da93
commit 19d5817f8f
30 changed files with 3669 additions and 19 deletions

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;