mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -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:
@ -1,6 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -8,10 +9,9 @@ namespace Bit.Core.Services
|
||||
{
|
||||
string SiteKey { get; }
|
||||
string SiteKeyResponseKeyName { get; }
|
||||
bool RequireCaptchaValidation(ICurrentContext currentContext, int failedLoginCount = 0);
|
||||
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
|
||||
bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null);
|
||||
Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress,
|
||||
User user = null);
|
||||
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.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
@ -37,14 +38,19 @@ namespace Bit.Core.Services
|
||||
|
||||
public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
|
||||
|
||||
public bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
|
||||
TokenIsApiKey(bypassToken, user) || TokenIsCaptchaBypassToken(bypassToken, user);
|
||||
|
||||
public async Task<bool> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress)
|
||||
public async Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
|
||||
User user = null)
|
||||
{
|
||||
var response = new CaptchaResponse { Success = false };
|
||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||
{
|
||||
return false;
|
||||
return response;
|
||||
}
|
||||
|
||||
if (user != null && ValidateCaptchaBypassToken(captchaResponse, user))
|
||||
{
|
||||
response.Success = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService");
|
||||
@ -70,39 +76,60 @@ namespace Bit.Core.Services
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(11389, e, "Unable to verify with HCaptcha.");
|
||||
return false;
|
||||
return response;
|
||||
}
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
return false;
|
||||
return response;
|
||||
}
|
||||
|
||||
using var jsonDocument = await responseMessage.Content.ReadFromJsonAsync<JsonDocument>();
|
||||
var root = jsonDocument.RootElement;
|
||||
return root.GetProperty("success").GetBoolean();
|
||||
using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync<HCaptchaResponse>();
|
||||
response.Success = hcaptchaResponse.Success;
|
||||
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 failedLoginCount = user?.FailedLoginCount ?? 0;
|
||||
var cloudEmailUnverified = !_globalSettings.SelfHosted && !user.EmailVerified;
|
||||
return currentContext.IsBot ||
|
||||
_globalSettings.Captcha.ForceCaptchaRequired ||
|
||||
cloudEmailUnverified ||
|
||||
failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling;
|
||||
}
|
||||
|
||||
public bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount)
|
||||
{
|
||||
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
||||
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
|
||||
}
|
||||
|
||||
private static bool TokenIsApiKey(string bypassToken, User user) =>
|
||||
private static bool TokenIsValidApiKey(string bypassToken, User user) =>
|
||||
!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) &&
|
||||
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 Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -8,13 +9,12 @@ namespace Bit.Core.Services
|
||||
{
|
||||
public string SiteKeyResponseKeyName => null;
|
||||
public string SiteKey => null;
|
||||
public bool RequireCaptchaValidation(ICurrentContext currentContext, int failedLoginCount = 0) => false;
|
||||
public bool ValidateFailedAuthEmailConditions(bool unknownDevice, int failedLoginCount) => false;
|
||||
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false;
|
||||
public string GenerateCaptchaBypassToken(User user) => "";
|
||||
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false;
|
||||
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
|
||||
public Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
|
||||
User user = null)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
return Task.FromResult(new CaptchaResponse { Success = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user