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:
parent
1796b1dd8e
commit
d2e48a5c2c
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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))
|
||||
{
|
||||
|
10
src/Core/Services/ICaptchaValidationService.cs
Normal file
10
src/Core/Services/ICaptchaValidationService.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user