mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 08:32:50 -05:00

This allows legitimate users to permanently bypass captcha once they've successfully logged in. Will allow unmonitored scripts more resilience to captcha requirements
109 lines
4.3 KiB
C#
109 lines
4.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using Bit.Core.Context;
|
|
using Bit.Core.Models.Table;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Utilities;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace Bit.Core.Services
|
|
{
|
|
public class HCaptchaValidationService : ICaptchaValidationService
|
|
{
|
|
private const double TokenLifetimeInHours = (double)5 / 60; // 5 minutes
|
|
private const string TokenName = "CaptchaBypassToken";
|
|
private const string TokenClearTextPrefix = "BWCaptchaBypass_";
|
|
private readonly ILogger<HCaptchaValidationService> _logger;
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
private readonly GlobalSettings _globalSettings;
|
|
private readonly IDataProtector _dataProtector;
|
|
|
|
public HCaptchaValidationService(
|
|
ILogger<HCaptchaValidationService> logger,
|
|
IHttpClientFactory httpClientFactory,
|
|
IDataProtectionProvider dataProtectorProvider,
|
|
GlobalSettings globalSettings)
|
|
{
|
|
_logger = logger;
|
|
_httpClientFactory = httpClientFactory;
|
|
_globalSettings = globalSettings;
|
|
_dataProtector = dataProtectorProvider.CreateProtector("CaptchaServiceDataProtector");
|
|
}
|
|
|
|
public string SiteKeyResponseKeyName => "HCaptcha_SiteKey";
|
|
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
|
|
|
|
public string GenerateCaptchaBypassToken(User user) =>
|
|
$"{TokenClearTextPrefix}{_dataProtector.Protect(CaptchaBypassTokenContent(user))}";
|
|
|
|
public bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
|
|
TokenIsApiKey(bypassToken, user) || TokenIsCaptchaBypassToken(bypassToken, user);
|
|
|
|
public async Task<bool> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(captchaResponse))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
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 false;
|
|
}
|
|
|
|
if (!responseMessage.IsSuccessStatusCode)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var responseContent = await responseMessage.Content.ReadAsStringAsync();
|
|
dynamic jsonResponse = JsonConvert.DeserializeObject(responseContent);
|
|
return (bool)jsonResponse.success;
|
|
}
|
|
|
|
public bool RequireCaptchaValidation(ICurrentContext currentContext) =>
|
|
currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
|
|
|
|
private static string CaptchaBypassTokenContent(User user) =>
|
|
string.Join(' ', new object[] {
|
|
TokenName,
|
|
user?.Id,
|
|
user?.Email,
|
|
CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow.AddHours(TokenLifetimeInHours))
|
|
});
|
|
|
|
private static bool TokenIsApiKey(string bypassToken, User user) =>
|
|
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
|
|
private bool TokenIsCaptchaBypassToken(string encryptedToken, User user) =>
|
|
encryptedToken.StartsWith(TokenClearTextPrefix) && user != null &&
|
|
CoreHelpers.TokenIsValid(TokenName, _dataProtector, encryptedToken[TokenClearTextPrefix.Length..],
|
|
user.Email, user.Id, TokenLifetimeInHours);
|
|
|
|
}
|
|
}
|