1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 05:28:15 -05: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 List<CurrentContentOrganization> Organizations { get; set; }
public virtual Guid? InstallationId { get; set; } public virtual Guid? InstallationId { get; set; }
public virtual Guid? OrganizationId { 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) public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings)
{ {
@ -49,6 +53,27 @@ namespace Bit.Core.Context
{ {
DeviceType = dType; 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) 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; return Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Custom) ?? false;
} }
public bool AccessBusinessPortal(Guid orgId) 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); && (o.Permissions?.AccessBusinessPortal ?? false)) ?? false);
} }
public bool AccessEventLogs(Guid orgId) 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); && (o.Permissions?.AccessEventLogs ?? false)) ?? false);
} }
public bool AccessImportExport(Guid orgId) 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); && (o.Permissions?.AccessImportExport ?? false)) ?? false);
} }
public bool AccessReports(Guid orgId) 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); && (o.Permissions?.AccessReports ?? false)) ?? false);
} }
public bool ManageAllCollections(Guid orgId) 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); && (o.Permissions?.ManageAllCollections ?? false)) ?? false);
} }
public bool ManageAssignedCollections(Guid orgId) 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); && (o.Permissions?.ManageAssignedCollections ?? false)) ?? false);
} }
public bool ManageGroups(Guid orgId) 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); && (o.Permissions?.ManageGroups ?? false)) ?? false);
} }
public bool ManagePolicies(Guid orgId) 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); && (o.Permissions?.ManagePolicies ?? false)) ?? false);
} }
public bool ManageSso(Guid orgId) 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); && (o.Permissions?.ManageSso ?? false)) ?? false);
} }
public bool ManageUsers(Guid orgId) 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); && (o.Permissions?.ManageUsers ?? false)) ?? false);
} }
public bool ManageResetPassword(Guid orgId) 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); && (o.Permissions?.ManageResetPassword ?? false)) ?? false);
} }
@ -283,9 +308,9 @@ namespace Bit.Core.Context
private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict) 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; claimsDict[claimKey].Any(x => x.Value == organizationId) : false;
} }

View File

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

View File

@ -20,6 +20,7 @@ namespace Bit.Core.IdentityServer
private UserManager<User> _userManager; private UserManager<User> _userManager;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICaptchaValidationService _captchaValidationService;
public ResourceOwnerPasswordValidator( public ResourceOwnerPasswordValidator(
UserManager<User> userManager, UserManager<User> userManager,
@ -35,7 +36,8 @@ namespace Bit.Core.IdentityServer
ILogger<ResourceOwnerPasswordValidator> logger, ILogger<ResourceOwnerPasswordValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IPolicyRepository policyRepository) IPolicyRepository policyRepository,
ICaptchaValidationService captchaValidationService)
: base(userManager, deviceRepository, deviceService, userService, eventService, : base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository) applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository)
@ -43,10 +45,39 @@ namespace Bit.Core.IdentityServer
_userManager = userManager; _userManager = userManager;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
_captchaValidationService = captchaValidationService;
} }
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) 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); await ValidateAsync(context, context.Request);
} }
@ -57,14 +88,6 @@ namespace Bit.Core.IdentityServer
return (null, false); 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()); var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
if (user == null || !await _userService.CheckPasswordAsync(user, context.Password)) 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 bool DisableEmailNewDevice { get; set; }
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
public virtual string EventGridKey { get; set; } public virtual string EventGridKey { get; set; }
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
public virtual InstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual InstallationSettings Installation { get; set; } = new InstallationSettings();
public virtual BaseServiceUriSettings BaseServiceUri { get; set; } public virtual BaseServiceUriSettings BaseServiceUri { get; set; }
public virtual SqlSettings SqlServer { get; set; } = new SqlSettings(); public virtual SqlSettings SqlServer { get; set; } = new SqlSettings();
@ -466,5 +467,11 @@ namespace Bit.Core.Settings
{ {
public int CacheLifetimeInSeconds { get; set; } = 60; 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>(); 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) public static void AddNoopServices(this IServiceCollection services)