mirror of
https://github.com/bitwarden/server.git
synced 2024-12-23 17:07:42 +01: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:
parent
259bf8d760
commit
8e1e2fa2fe
@ -88,7 +88,8 @@ namespace Bit.CommCore.Services
|
||||
throw new BadRequestException("Provider is already setup.");
|
||||
}
|
||||
|
||||
if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id, _globalSettings))
|
||||
if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id,
|
||||
_globalSettings.OrganizationInviteExpirationHours))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
@ -196,7 +197,8 @@ namespace Bit.CommCore.Services
|
||||
throw new BadRequestException("Already accepted.");
|
||||
}
|
||||
|
||||
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _dataProtector, token, user.Email, providerUser.Id, _globalSettings))
|
||||
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _dataProtector, token, user.Email, providerUser.Id,
|
||||
_globalSettings.OrganizationInviteExpirationHours))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ namespace Bit.Core.IdentityServer
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
|
||||
public ResourceOwnerPasswordValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
@ -60,25 +59,36 @@ namespace Bit.Core.IdentityServer
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (_captchaValidationService.ServiceEnabled && _currentContext.IsBot)
|
||||
string bypassToken = null;
|
||||
if (_captchaValidationService.ServiceEnabled && (_currentContext.IsBot || _captchaValidationService.RequireCaptcha))
|
||||
{
|
||||
var captchaResponse = context.Request.Raw["CaptchaResponse"]?.ToString();
|
||||
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
||||
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.");
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
|
||||
new Dictionary<string, object> {
|
||||
{ "HCaptcha_SiteKey", _captchaValidationService.SiteKey },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var captchaValid = await _captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
|
||||
_currentContext.IpAddress);
|
||||
var captchaValid = _captchaValidationService.ValidateCaptchaBypassToken(captchaResponse, user) ||
|
||||
await _captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse, _currentContext.IpAddress);
|
||||
if (!captchaValid)
|
||||
{
|
||||
await BuildErrorResultAsync("Captcha is invalid.", false, context, null);
|
||||
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
|
||||
return;
|
||||
}
|
||||
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
}
|
||||
|
||||
await ValidateAsync(context, context.Request);
|
||||
if (context.Result.CustomResponse != null && bypassToken != null)
|
||||
{
|
||||
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
|
||||
}
|
||||
}
|
||||
|
||||
protected async override Task<(User, bool)> ValidateContextAsync(ResourceOwnerPasswordValidationContext context)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -473,6 +473,7 @@ namespace Bit.Core.Settings
|
||||
|
||||
public class CaptchaSettings
|
||||
{
|
||||
public bool RequireCaptcha { get; set; } = false;
|
||||
public string HCaptchaSecretKey { get; set; }
|
||||
public string HCaptchaSiteKey { get; set; }
|
||||
}
|
||||
|
@ -619,11 +619,12 @@ namespace Bit.Core.Utilities
|
||||
public static bool UserInviteTokenIsValid(IDataProtector protector, string token, string userEmail,
|
||||
Guid orgUserId, GlobalSettings globalSettings)
|
||||
{
|
||||
return TokenIsValid("OrganizationUserInvite", protector, token, userEmail, orgUserId, globalSettings);
|
||||
return TokenIsValid("OrganizationUserInvite", protector, token, userEmail, orgUserId,
|
||||
globalSettings.OrganizationInviteExpirationHours);
|
||||
}
|
||||
|
||||
public static bool TokenIsValid(string firstTokenPart, IDataProtector protector, string token, string userEmail,
|
||||
Guid id, GlobalSettings globalSettings)
|
||||
Guid id, double expirationInHours)
|
||||
{
|
||||
var invalid = true;
|
||||
try
|
||||
@ -635,7 +636,7 @@ namespace Bit.Core.Utilities
|
||||
dataParts[2].Equals(userEmail, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var creationTime = FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));
|
||||
var expTime = creationTime.AddHours(globalSettings.OrganizationInviteExpirationHours);
|
||||
var expTime = creationTime.AddHours(expirationInHours);
|
||||
invalid = expTime < DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
@ -304,7 +304,7 @@ namespace Bit.Core.Utilities
|
||||
services.AddSingleton<IReferenceEventService, AzureQueueReferenceEventService>();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) &&
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey))
|
||||
{
|
||||
services.AddSingleton<ICaptchaValidationService, HCaptchaValidationService>();
|
||||
|
Loading…
Reference in New Issue
Block a user