mirror of
https://github.com/bitwarden/server.git
synced 2025-07-20 17:11:36 -05:00
chore(captcha): [PM-15162] Remove captcha enforcement and issuing of bypass token
* Remove captcha enforcement and issuing/verification of bypass token * Removed more captcha logic. * Removed logic to enforce failed login attempts * Linting. * Fixed order of initialization. * Fixed merge conflicts * Renamed registration finish response for clarity * Remove unnecessary mailService references.
This commit is contained in:
@ -1,6 +0,0 @@
|
||||
namespace Bit.Core.Auth.Models.Api;
|
||||
|
||||
public interface ICaptchaProtectedModel
|
||||
{
|
||||
string CaptchaResponse { get; set; }
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
namespace Bit.Core.Auth.Models.Business;
|
||||
|
||||
public class CaptchaResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public bool MaybeBot { get; set; }
|
||||
public bool IsBot { get; set; }
|
||||
public double Score { get; set; }
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tokens;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Business.Tokenables;
|
||||
|
||||
public class HCaptchaTokenable : ExpiringTokenable
|
||||
{
|
||||
private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes
|
||||
public const string ClearTextPrefix = "BWCaptchaBypass_";
|
||||
public const string DataProtectorPurpose = "CaptchaServiceDataProtector";
|
||||
public const string TokenIdentifier = "CaptchaBypassToken";
|
||||
|
||||
public string Identifier { get; set; } = TokenIdentifier;
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public HCaptchaTokenable()
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);
|
||||
}
|
||||
|
||||
public HCaptchaTokenable(User user) : this()
|
||||
{
|
||||
Id = user?.Id ?? default;
|
||||
Email = user?.Email;
|
||||
}
|
||||
|
||||
public bool TokenIsValid(User user)
|
||||
{
|
||||
if (Id == default || Email == default || user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Id == user.Id &&
|
||||
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
// Validates deserialized
|
||||
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public interface ICaptchaValidationService
|
||||
{
|
||||
string SiteKey { get; }
|
||||
string SiteKeyResponseKeyName { get; }
|
||||
bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null);
|
||||
Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress,
|
||||
User user = null);
|
||||
string GenerateCaptchaBypassToken(User user);
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public class HCaptchaValidationService : ICaptchaValidationService
|
||||
{
|
||||
private readonly ILogger<HCaptchaValidationService> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IDataProtectorTokenFactory<HCaptchaTokenable> _tokenizer;
|
||||
|
||||
public HCaptchaValidationService(
|
||||
ILogger<HCaptchaValidationService> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDataProtectorTokenFactory<HCaptchaTokenable> tokenizer,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_globalSettings = globalSettings;
|
||||
_tokenizer = tokenizer;
|
||||
}
|
||||
|
||||
public string SiteKeyResponseKeyName => "HCaptcha_SiteKey";
|
||||
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
|
||||
|
||||
public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
|
||||
|
||||
public async Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
|
||||
User user = null)
|
||||
{
|
||||
var response = new CaptchaResponse { Success = false };
|
||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
if (user != null && ValidateCaptchaBypassToken(captchaResponse, user))
|
||||
{
|
||||
response.Success = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService");
|
||||
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri("https://hcaptcha.com/siteverify"),
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) },
|
||||
{ "secret", _globalSettings.Captcha.HCaptchaSecretKey },
|
||||
{ "sitekey", SiteKey },
|
||||
{ "remoteip", clientIpAddress }
|
||||
})
|
||||
};
|
||||
|
||||
HttpResponseMessage responseMessage;
|
||||
try
|
||||
{
|
||||
responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(11389, e, "Unable to verify with HCaptcha.");
|
||||
return response;
|
||||
}
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
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;
|
||||
response.Score = score;
|
||||
return response;
|
||||
}
|
||||
|
||||
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 requireOnCloud = !_globalSettings.SelfHosted && !user.EmailVerified &&
|
||||
user.CreationDate < DateTime.UtcNow.AddHours(-24);
|
||||
return currentContext.IsBot ||
|
||||
_globalSettings.Captcha.ForceCaptchaRequired ||
|
||||
requireOnCloud ||
|
||||
failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling;
|
||||
}
|
||||
|
||||
private static bool TokenIsValidApiKey(string bypassToken, User user) =>
|
||||
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
|
||||
|
||||
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,18 +0,0 @@
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public class NoopCaptchaValidationService : ICaptchaValidationService
|
||||
{
|
||||
public string SiteKeyResponseKeyName => null;
|
||||
public string SiteKey => null;
|
||||
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false;
|
||||
public string GenerateCaptchaBypassToken(User user) => "";
|
||||
public Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
|
||||
User user = null)
|
||||
{
|
||||
return Task.FromResult(new CaptchaResponse { Success = true });
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
using Bit.Core.Auth.Models.Api;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Utilities;
|
||||
|
||||
public class CaptchaProtectedAttribute : ActionFilterAttribute
|
||||
{
|
||||
public string ModelParameterName { get; set; } = "model";
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();
|
||||
var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService<ICaptchaValidationService>();
|
||||
|
||||
if (captchaValidationService.RequireCaptchaValidation(currentContext, null))
|
||||
{
|
||||
var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||
{
|
||||
throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey);
|
||||
}
|
||||
|
||||
var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
|
||||
currentContext.IpAddress, null).GetAwaiter().GetResult();
|
||||
if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot)
|
||||
{
|
||||
throw new BadRequestException("Captcha is invalid. Please refresh and try again");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user