1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

captcha scores (#1967)

* captcha scores

* some api fixes

* check bot on captcha attribute

* Update src/Core/Services/Implementations/HCaptchaValidationService.cs

Co-authored-by: e271828- <e271828-@users.noreply.github.com>

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>
This commit is contained in:
Kyle Spearrin
2022-05-09 12:25:13 -04:00
committed by GitHub
parent a5bfc0554b
commit 3ffd240287
10 changed files with 138 additions and 66 deletions

View File

@ -12,6 +12,7 @@ using Bit.Core.Enums;
using Bit.Core.Identity;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -78,18 +79,25 @@ namespace Bit.Core.IdentityServer
_captchaValidationService = captchaValidationService;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request, bool unknownDevice = false)
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
CustomValidatorRequestContext validatorContext)
{
var isBot = (validatorContext.CaptchaResponse?.IsBot ?? false);
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
!string.IsNullOrWhiteSpace(twoFactorProvider);
var (user, valid) = await ValidateContextAsync(context);
var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User;
if (!valid)
{
await UpdateFailedAuthDetailsAsync(user, false, unknownDevice);
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
}
if (!valid || isBot)
{
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
return;
}
@ -112,13 +120,13 @@ namespace Bit.Core.IdentityServer
AfterVerifyTwoFactor(user, twoFactorProviderType, requires2FABecauseNewDevice);
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
if ((!verified || isBot) && twoFactorProviderType != TwoFactorProviderType.Remember)
{
await UpdateFailedAuthDetailsAsync(user, true, unknownDevice);
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
return;
}
else if (!verified && twoFactorProviderType == TwoFactorProviderType.Remember)
else if ((!verified || isBot) && twoFactorProviderType == TwoFactorProviderType.Remember)
{
// Delay for brute force.
await Task.Delay(2000);
@ -153,7 +161,7 @@ namespace Bit.Core.IdentityServer
}
}
protected abstract Task<(User, bool)> ValidateContextAsync(T context);
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
{
@ -407,9 +415,7 @@ namespace Bit.Core.IdentityServer
private void BeforeVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
{
if (type == TwoFactorProviderType.Email
&&
requires2FABecauseNewDevice)
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
@ -424,9 +430,7 @@ namespace Bit.Core.IdentityServer
private void AfterVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
{
if (type == TwoFactorProviderType.Email
&&
requires2FABecauseNewDevice)
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
{
user.ClearTwoFactorProviders();
}
@ -595,7 +599,7 @@ namespace Bit.Core.IdentityServer
user.LastFailedLoginDate = user.RevisionDate = utcNow;
await _userRepository.ReplaceAsync(user);
if (_captchaValidationService.ValidateFailedAuthEmailConditions(unknownDevice, user.FailedLoginCount))
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
{
if (twoFactorInvalid)
{
@ -607,5 +611,12 @@ namespace Bit.Core.IdentityServer
}
}
}
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
{
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
var failedLoginCount = user?.FailedLoginCount ?? 0;
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
}
}
}

View File

@ -63,15 +63,20 @@ namespace Bit.Core.IdentityServer
{
return;
}
await ValidateAsync(context, context.Result.ValidatedRequest);
await ValidateAsync(context, context.Result.ValidatedRequest,
new CustomValidatorRequestContext { KnownDevice = true });
}
protected async override Task<(User, bool)> ValidateContextAsync(CustomTokenRequestValidationContext context)
protected async override Task<bool> ValidateContextAsync(CustomTokenRequestValidationContext context,
CustomValidatorRequestContext validatorContext)
{
var email = context.Result.ValidatedRequest.Subject?.GetDisplayName()
?? context.Result.ValidatedRequest.ClientClaims?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
var user = string.IsNullOrWhiteSpace(email) ? null : await _userManager.FindByEmailAsync(email);
return (user, user != null);
if (!string.IsNullOrWhiteSpace(email))
{
validatorContext.User = await _userManager.FindByEmailAsync(email);
}
return validatorContext.User != null;
}
protected override async Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,

View File

@ -0,0 +1,12 @@
using Bit.Core.Entities;
using Bit.Core.Models.Business;
namespace Bit.Core.IdentityServer
{
public class CustomValidatorRequestContext
{
public User User { get; set; }
public bool KnownDevice { get; set; }
public CaptchaResponse CaptchaResponse { get; set; }
}
}

View File

@ -59,25 +59,31 @@ namespace Bit.Core.IdentityServer
return;
}
string bypassToken = null;
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
var unknownDevice = !await KnownDeviceAsync(user, context.Request);
if (unknownDevice && _captchaValidationService.RequireCaptchaValidation(_currentContext, user?.FailedLoginCount ?? 0))
var validatorContext = new CustomValidatorRequestContext
{
User = user,
KnownDevice = await KnownDeviceAsync(user, context.Request)
};
string bypassToken = null;
if (!validatorContext.KnownDevice &&
_captchaValidationService.RequireCaptchaValidation(_currentContext, user))
{
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
if (string.IsNullOrWhiteSpace(captchaResponse))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
new Dictionary<string, object> {
new Dictionary<string, object>
{
{ _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
});
return;
}
var captchaValid = _captchaValidationService.ValidateCaptchaBypassToken(captchaResponse, user) ||
await _captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse, _currentContext.IpAddress);
if (!captchaValid)
validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync(
captchaResponse, _currentContext.IpAddress, null);
if (!validatorContext.CaptchaResponse.Success)
{
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
return;
@ -85,27 +91,27 @@ namespace Bit.Core.IdentityServer
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
}
await ValidateAsync(context, context.Request, unknownDevice);
await ValidateAsync(context, context.Request, validatorContext);
if (context.Result.CustomResponse != null && bypassToken != null)
{
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
}
}
protected async override Task<(User, bool)> ValidateContextAsync(ResourceOwnerPasswordValidationContext context)
protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,
CustomValidatorRequestContext validatorContext)
{
if (string.IsNullOrWhiteSpace(context.UserName))
if (string.IsNullOrWhiteSpace(context.UserName) || validatorContext.User == null)
{
return (null, false);
return false;
}
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
if (user == null || !await _userService.CheckPasswordAsync(user, context.Password))
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
{
return (user, false);
return false;
}
return (user, true);
return true;
}
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,