mirror of
https://github.com/bitwarden/server.git
synced 2025-02-16 01:51:21 +01: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:
parent
a5bfc0554b
commit
3ffd240287
@ -12,6 +12,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -78,18 +79,25 @@ namespace Bit.Core.IdentityServer
|
|||||||
_captchaValidationService = captchaValidationService;
|
_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 twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||||
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||||
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
||||||
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||||
|
|
||||||
var (user, valid) = await ValidateContextAsync(context);
|
var valid = await ValidateContextAsync(context, validatorContext);
|
||||||
|
var user = validatorContext.User;
|
||||||
if (!valid)
|
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);
|
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -112,13 +120,13 @@ namespace Bit.Core.IdentityServer
|
|||||||
|
|
||||||
AfterVerifyTwoFactor(user, twoFactorProviderType, requires2FABecauseNewDevice);
|
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);
|
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (!verified && twoFactorProviderType == TwoFactorProviderType.Remember)
|
else if ((!verified || isBot) && twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||||
{
|
{
|
||||||
// Delay for brute force.
|
// Delay for brute force.
|
||||||
await Task.Delay(2000);
|
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)
|
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)
|
private void BeforeVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
||||||
{
|
{
|
||||||
if (type == TwoFactorProviderType.Email
|
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
|
||||||
&&
|
|
||||||
requires2FABecauseNewDevice)
|
|
||||||
{
|
{
|
||||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
{
|
{
|
||||||
@ -424,9 +430,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
|
|
||||||
private void AfterVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
private void AfterVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
||||||
{
|
{
|
||||||
if (type == TwoFactorProviderType.Email
|
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
|
||||||
&&
|
|
||||||
requires2FABecauseNewDevice)
|
|
||||||
{
|
{
|
||||||
user.ClearTwoFactorProviders();
|
user.ClearTwoFactorProviders();
|
||||||
}
|
}
|
||||||
@ -595,7 +599,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
|
|
||||||
if (_captchaValidationService.ValidateFailedAuthEmailConditions(unknownDevice, user.FailedLoginCount))
|
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
|
||||||
{
|
{
|
||||||
if (twoFactorInvalid)
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,15 +63,20 @@ namespace Bit.Core.IdentityServer
|
|||||||
{
|
{
|
||||||
return;
|
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()
|
var email = context.Result.ValidatedRequest.Subject?.GetDisplayName()
|
||||||
?? context.Result.ValidatedRequest.ClientClaims?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
|
?? context.Result.ValidatedRequest.ClientClaims?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
|
||||||
var user = string.IsNullOrWhiteSpace(email) ? null : await _userManager.FindByEmailAsync(email);
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
return (user, user != null);
|
{
|
||||||
|
validatorContext.User = await _userManager.FindByEmailAsync(email);
|
||||||
|
}
|
||||||
|
return validatorContext.User != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,
|
protected override async Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,
|
||||||
|
12
src/Core/IdentityServer/CustomValidatorRequestContext.cs
Normal file
12
src/Core/IdentityServer/CustomValidatorRequestContext.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -59,25 +59,31 @@ namespace Bit.Core.IdentityServer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 validatorContext = new CustomValidatorRequestContext
|
||||||
if (unknownDevice && _captchaValidationService.RequireCaptchaValidation(_currentContext, user?.FailedLoginCount ?? 0))
|
{
|
||||||
|
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();
|
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||||
{
|
{
|
||||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
|
||||||
new Dictionary<string, object> {
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
{ _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
|
{ _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var captchaValid = _captchaValidationService.ValidateCaptchaBypassToken(captchaResponse, user) ||
|
validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync(
|
||||||
await _captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse, _currentContext.IpAddress);
|
captchaResponse, _currentContext.IpAddress, null);
|
||||||
if (!captchaValid)
|
if (!validatorContext.CaptchaResponse.Success)
|
||||||
{
|
{
|
||||||
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
|
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
|
||||||
return;
|
return;
|
||||||
@ -85,27 +91,27 @@ namespace Bit.Core.IdentityServer
|
|||||||
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ValidateAsync(context, context.Request, unknownDevice);
|
await ValidateAsync(context, context.Request, validatorContext);
|
||||||
if (context.Result.CustomResponse != null && bypassToken != null)
|
if (context.Result.CustomResponse != null && bypassToken != null)
|
||||||
{
|
{
|
||||||
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
|
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 (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
||||||
if (user == null || !await _userService.CheckPasswordAsync(user, context.Password))
|
|
||||||
{
|
{
|
||||||
return (user, false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (user, true);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
||||||
|
9
src/Core/Models/Business/CaptchaResponse.cs
Normal file
9
src/Core/Models/Business/CaptchaResponse.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Bit.Core.Models.Business
|
||||||
|
{
|
||||||
|
public class CaptchaResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public bool MaybeBot { get; set; }
|
||||||
|
public bool IsBot { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -8,10 +9,9 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
string SiteKey { get; }
|
string SiteKey { get; }
|
||||||
string SiteKeyResponseKeyName { get; }
|
string SiteKeyResponseKeyName { get; }
|
||||||
bool RequireCaptchaValidation(ICurrentContext currentContext, int failedLoginCount = 0);
|
bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null);
|
||||||
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
|
Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress,
|
||||||
|
User user = null);
|
||||||
string GenerateCaptchaBypassToken(User user);
|
string GenerateCaptchaBypassToken(User user);
|
||||||
bool ValidateCaptchaBypassToken(string encryptedToken, User user);
|
|
||||||
bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Business.Tokenables;
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -37,14 +38,19 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
|
public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
|
||||||
|
|
||||||
public bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
|
public async Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
|
||||||
TokenIsApiKey(bypassToken, user) || TokenIsCaptchaBypassToken(bypassToken, user);
|
User user = null)
|
||||||
|
|
||||||
public async Task<bool> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress)
|
|
||||||
{
|
{
|
||||||
|
var response = new CaptchaResponse { Success = false };
|
||||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||||
{
|
{
|
||||||
return false;
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user != null && ValidateCaptchaBypassToken(captchaResponse, user))
|
||||||
|
{
|
||||||
|
response.Success = true;
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService");
|
var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService");
|
||||||
@ -70,39 +76,60 @@ namespace Bit.Core.Services
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(11389, e, "Unable to verify with HCaptcha.");
|
_logger.LogError(11389, e, "Unable to verify with HCaptcha.");
|
||||||
return false;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!responseMessage.IsSuccessStatusCode)
|
if (!responseMessage.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return false;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var jsonDocument = await responseMessage.Content.ReadFromJsonAsync<JsonDocument>();
|
using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync<HCaptchaResponse>();
|
||||||
var root = jsonDocument.RootElement;
|
response.Success = hcaptchaResponse.Success;
|
||||||
return root.GetProperty("success").GetBoolean();
|
var score = hcaptchaResponse.Score.GetValueOrDefault();
|
||||||
|
response.MaybeBot = score >= _globalSettings.Captcha.MaybeBotScoreThreshold;
|
||||||
|
response.IsBot = score >= _globalSettings.Captcha.IsBotScoreThreshold;
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RequireCaptchaValidation(ICurrentContext currentContext, int failedLoginCount = 0)
|
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null)
|
||||||
{
|
{
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
|
||||||
|
}
|
||||||
|
|
||||||
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
||||||
|
var failedLoginCount = user?.FailedLoginCount ?? 0;
|
||||||
|
var cloudEmailUnverified = !_globalSettings.SelfHosted && !user.EmailVerified;
|
||||||
return currentContext.IsBot ||
|
return currentContext.IsBot ||
|
||||||
_globalSettings.Captcha.ForceCaptchaRequired ||
|
_globalSettings.Captcha.ForceCaptchaRequired ||
|
||||||
|
cloudEmailUnverified ||
|
||||||
failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling;
|
failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount)
|
private static bool TokenIsValidApiKey(string bypassToken, User user) =>
|
||||||
{
|
|
||||||
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
|
||||||
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TokenIsApiKey(string bypassToken, User user) =>
|
|
||||||
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
|
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
|
||||||
private bool TokenIsCaptchaBypassToken(string encryptedToken, User user)
|
|
||||||
|
private bool TokenIsValidCaptchaBypassToken(string encryptedToken, User user)
|
||||||
{
|
{
|
||||||
return _tokenizer.TryUnprotect(encryptedToken, out var data) &&
|
return _tokenizer.TryUnprotect(encryptedToken, out var data) &&
|
||||||
data.Valid && data.TokenIsValid(user);
|
data.Valid && data.TokenIsValid(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
|
||||||
|
TokenIsValidApiKey(bypassToken, user) || TokenIsValidCaptchaBypassToken(bypassToken, user);
|
||||||
|
|
||||||
|
public class HCaptchaResponse : IDisposable
|
||||||
|
{
|
||||||
|
[JsonPropertyName("success")]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
[JsonPropertyName("score")]
|
||||||
|
public double? Score { get; set; }
|
||||||
|
[JsonPropertyName("score_reason")]
|
||||||
|
public List<string> ScoreReason { get; set; }
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -8,13 +9,12 @@ 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, int failedLoginCount = 0) => false;
|
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => 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 Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
|
||||||
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
|
User user = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(new CaptchaResponse { Success = true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -469,6 +469,8 @@ namespace Bit.Core.Settings
|
|||||||
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 int MaximumFailedLoginAttempts { get; set; }
|
||||||
|
public double MaybeBotScoreThreshold { get; set; } = double.MaxValue;
|
||||||
|
public double IsBotScoreThreshold { get; set; } = double.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StripeSettings
|
public class StripeSettings
|
||||||
|
@ -16,7 +16,7 @@ namespace Bit.Core.Utilities
|
|||||||
var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();
|
var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();
|
||||||
var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService<ICaptchaValidationService>();
|
var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService<ICaptchaValidationService>();
|
||||||
|
|
||||||
if (captchaValidationService.RequireCaptchaValidation(currentContext))
|
if (captchaValidationService.RequireCaptchaValidation(currentContext, null))
|
||||||
{
|
{
|
||||||
var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse;
|
var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse;
|
||||||
|
|
||||||
@ -25,9 +25,9 @@ namespace Bit.Core.Utilities
|
|||||||
throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey);
|
throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
var captchaValid = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
|
var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
|
||||||
currentContext.IpAddress).GetAwaiter().GetResult();
|
currentContext.IpAddress, null).GetAwaiter().GetResult();
|
||||||
if (!captchaValid)
|
if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Captcha is invalid. Please refresh and try again");
|
throw new BadRequestException("Captcha is invalid. Please refresh and try again");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user