1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-22 02:51:33 +01:00

hcaptcha validation on password login (#1398)

This commit is contained in:
Kyle Spearrin 2021-06-16 12:47:41 -04:00 committed by GitHub
parent 1796b1dd8e
commit d2e48a5c2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 189 additions and 25 deletions

View File

@ -27,6 +27,10 @@ namespace Bit.Core.Context
public virtual List<CurrentContentOrganization> Organizations { get; set; }
public virtual Guid? InstallationId { get; set; }
public virtual Guid? OrganizationId { get; set; }
public virtual bool CloudflareWorkerProxied { get; set; }
public virtual bool IsBot { get; set; }
public virtual bool MaybeBot { get; set; }
public virtual int? BotScore { get; set; }
public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings)
{
@ -49,6 +53,27 @@ namespace Bit.Core.Context
{
DeviceType = dType;
}
if (!BotScore.HasValue && httpContext.Request.Headers.ContainsKey("X-Cf-Bot-Score") &&
int.TryParse(httpContext.Request.Headers["X-Cf-Bot-Score"], out var parsedBotScore))
{
BotScore = parsedBotScore;
}
if (httpContext.Request.Headers.ContainsKey("X-Cf-Worked-Proxied"))
{
CloudflareWorkerProxied = httpContext.Request.Headers["X-Cf-Worked-Proxied"] == "1";
}
if (httpContext.Request.Headers.ContainsKey("X-Cf-Is-Bot"))
{
IsBot = httpContext.Request.Headers["X-Cf-Is-Bot"] == "1";
}
if (httpContext.Request.Headers.ContainsKey("X-Cf-Maybe-Bot"))
{
MaybeBot = httpContext.Request.Headers["X-Cf-Maybe-Bot"] == "1";
}
}
public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings)
@ -192,70 +217,70 @@ namespace Bit.Core.Context
{
return Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Custom) ?? false;
}
public bool AccessBusinessPortal(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.AccessBusinessPortal ?? false)) ?? false);
}
public bool AccessEventLogs(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.AccessEventLogs ?? false)) ?? false);
}
public bool AccessImportExport(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.AccessImportExport ?? false)) ?? false);
}
public bool AccessReports(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.AccessReports ?? false)) ?? false);
}
public bool ManageAllCollections(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageAllCollections ?? false)) ?? false);
}
public bool ManageAssignedCollections(Guid orgId)
{
return OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageAssignedCollections ?? false)) ?? false);
}
public bool ManageGroups(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageGroups ?? false)) ?? false);
}
public bool ManagePolicies(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManagePolicies ?? false)) ?? false);
}
public bool ManageSso(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageSso ?? false)) ?? false);
}
public bool ManageUsers(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageUsers ?? false)) ?? false);
}
public bool ManageResetPassword(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageResetPassword ?? false)) ?? false);
}
@ -283,9 +308,9 @@ namespace Bit.Core.Context
private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict)
{
bool hasClaim(string claimKey)
bool hasClaim(string claimKey)
{
return claimsDict.ContainsKey(claimKey) ?
return claimsDict.ContainsKey(claimKey) ?
claimsDict[claimKey].Any(x => x.Value == organizationId) : false;
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
@ -21,6 +21,9 @@ namespace Bit.Core.Context
List<CurrentContentOrganization> Organizations { get; set; }
Guid? InstallationId { get; set; }
Guid? OrganizationId { get; set; }
bool IsBot { get; set; }
bool MaybeBot { get; set; }
int? BotScore { get; set; }
Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings);
Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings);

View File

@ -20,6 +20,7 @@ namespace Bit.Core.IdentityServer
private UserManager<User> _userManager;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
private readonly ICaptchaValidationService _captchaValidationService;
public ResourceOwnerPasswordValidator(
UserManager<User> userManager,
@ -35,7 +36,8 @@ namespace Bit.Core.IdentityServer
ILogger<ResourceOwnerPasswordValidator> logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
IPolicyRepository policyRepository)
IPolicyRepository policyRepository,
ICaptchaValidationService captchaValidationService)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository)
@ -43,10 +45,39 @@ namespace Bit.Core.IdentityServer
_userManager = userManager;
_userService = userService;
_currentContext = currentContext;
_captchaValidationService = captchaValidationService;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
// Uncomment whenever we want to require the `auth-email` header
//
//if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Auth-Email") ||
// _currentContext.HttpContext.Request.Headers["Auth-Email"] != context.UserName)
//{
// context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
// "Auth-Email header invalid.");
// return;
//}
if (_captchaValidationService.ServiceEnabled && _currentContext.IsBot)
{
var captchaResponse = context.Request.Raw["CaptchaResponse"]?.ToString();
if (string.IsNullOrWhiteSpace(captchaResponse))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.");
return;
}
var captchaValid = await _captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
_currentContext.IpAddress);
if (!captchaValid)
{
await BuildErrorResultAsync("Captcha is invalid.", false, context, null);
return;
}
}
await ValidateAsync(context, context.Request);
}
@ -57,14 +88,6 @@ namespace Bit.Core.IdentityServer
return (null, false);
}
// Uncomment whenever we want to require the `auth-email` header
//
//if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Auth-Email") ||
// _currentContext.HttpContext.Request.Headers["Auth-Email"] != context.UserName)
//{
// return (null, false);
//}
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
if (user == null || !await _userService.CheckPasswordAsync(user, context.Password))
{

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public interface ICaptchaValidationService
{
bool ServiceEnabled { get; }
Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress);
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Bit.Core.Services
{
public class HCaptchaValidationService : ICaptchaValidationService
{
private readonly ILogger<HCaptchaValidationService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly GlobalSettings _globalSettings;
public HCaptchaValidationService(
ILogger<HCaptchaValidationService> logger,
IHttpClientFactory httpClientFactory,
GlobalSettings globalSettings)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_globalSettings = globalSettings;
}
public bool ServiceEnabled => true;
public async Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
{
if (string.IsNullOrWhiteSpace(captchResponse))
{
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", captchResponse.TrimStart("hcaptcha|".ToCharArray()) },
{ "secret", _globalSettings.Captcha.HCaptchaSecretKey },
{ "sitekey", _globalSettings.Captcha.HCaptchaSiteKey },
{ "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;
}
}
}

View File

@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class NoopCaptchaValidationService : ICaptchaValidationService
{
public bool ServiceEnabled => false;
public Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress)
{
return Task.FromResult(true);
}
}
}

View File

@ -40,6 +40,7 @@ namespace Bit.Core.Settings
public virtual bool DisableEmailNewDevice { get; set; }
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
public virtual string EventGridKey { get; set; }
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
public virtual InstallationSettings Installation { get; set; } = new InstallationSettings();
public virtual BaseServiceUriSettings BaseServiceUri { get; set; }
public virtual SqlSettings SqlServer { get; set; } = new SqlSettings();
@ -466,5 +467,11 @@ namespace Bit.Core.Settings
{
public int CacheLifetimeInSeconds { get; set; } = 60;
}
public class CaptchaSettings
{
public string HCaptchaSecretKey { get; set; }
public string HCaptchaSiteKey { get; set; }
}
}
}

View File

@ -253,6 +253,16 @@ namespace Bit.Core.Utilities
{
services.AddSingleton<IReferenceEventService, AzureQueueReferenceEventService>();
}
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) &&
CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey))
{
services.AddSingleton<ICaptchaValidationService, HCaptchaValidationService>();
}
else
{
services.AddSingleton<ICaptchaValidationService, NoopCaptchaValidationService>();
}
}
public static void AddNoopServices(this IServiceCollection services)