1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 16:42:50 -05:00

Feature/sync Enable hcaptcha on login (#1469)

* Share globalSettings hcaptcha public key with clients

* Require captcha valid only prior to two factor

users with two factor will have already solved captcha is necessary.
Users without two factor will have`TwoFactorVerified` set to false

* Do not require CaptchaResponse on two-factor requests

* Add option to always require captcha for testing purposes

* Allow for self-hosted instances if they want to use it

* Move refresh suggestion to correct error

* Expect lifetime in helper method

* Add captcha bypass token to successful captcha validations

* Remove twofactorValidated

* PR Feedback
This commit is contained in:
Matt Gibson
2021-07-21 13:42:06 -05:00
committed by GitHub
parent 259bf8d760
commit 8e1e2fa2fe
9 changed files with 67 additions and 15 deletions

View File

@ -1,10 +1,15 @@
using System.Threading.Tasks;
using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
public interface ICaptchaValidationService
{
bool ServiceEnabled { get; }
string SiteKey { get; }
bool RequireCaptcha { get; }
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
string GenerateCaptchaBypassToken(User user);
bool ValidateCaptchaBypassToken(string encryptedToken, User user);
}
}

View File

@ -115,7 +115,8 @@ namespace Bit.Core.Services
throw new BadRequestException("Emergency Access not valid.");
}
if (!CoreHelpers.TokenIsValid("EmergencyAccessInvite", _dataProtector, token, user.Email, emergencyAccessId, _globalSettings))
if (!CoreHelpers.TokenIsValid("EmergencyAccessInvite", _dataProtector, token, user.Email, emergencyAccessId,
_globalSettings.OrganizationInviteExpirationHours))
{
throw new BadRequestException("Invalid token.");
}

View File

@ -2,7 +2,10 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@ -10,21 +13,36 @@ 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 bool ServiceEnabled => true;
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
public bool RequireCaptcha => _globalSettings.Captcha.RequireCaptcha;
public string GenerateCaptchaBypassToken(User user) =>
$"{TokenClearTextPrefix}{_dataProtector.Protect(CaptchaBypassTokenContent(user))}";
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) =>
encryptedToken.StartsWith(TokenClearTextPrefix) && user != null &&
CoreHelpers.TokenIsValid(TokenName, _dataProtector, encryptedToken[TokenClearTextPrefix.Length..],
user.Email, user.Id, TokenLifetimeInHours);
public async Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
{
@ -43,7 +61,7 @@ namespace Bit.Core.Services
{
{ "response", captchResponse.TrimStart("hcaptcha|".ToCharArray()) },
{ "secret", _globalSettings.Captcha.HCaptchaSecretKey },
{ "sitekey", _globalSettings.Captcha.HCaptchaSiteKey },
{ "sitekey", SiteKey },
{ "remoteip", clientIpAddress }
})
};
@ -68,5 +86,13 @@ namespace Bit.Core.Services
dynamic jsonResponse = JsonConvert.DeserializeObject(responseContent);
return (bool)jsonResponse.success;
}
private static string CaptchaBypassTokenContent(User user) =>
string.Join(' ', new object[] {
TokenName,
user?.Id,
user?.Email,
CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow.AddHours(TokenLifetimeInHours))
});
}
}

View File

@ -1,10 +1,16 @@
using System.Threading.Tasks;
using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
public class NoopCaptchaValidationService : ICaptchaValidationService
{
public bool ServiceEnabled => false;
public string SiteKey => null;
public bool RequireCaptcha => false;
public string GenerateCaptchaBypassToken(User user) => "";
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false;
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
{