1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-26 12:55:17 +01:00

Protect user registration with captcha (#1480)

* Protect user registration with captcha

* PR feedback
This commit is contained in:
Matt Gibson 2021-07-22 12:29:06 -05:00 committed by GitHub
parent 46fa6f6673
commit 7a135ae7cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 64 additions and 13 deletions

View File

@ -83,6 +83,7 @@ namespace Bit.Api.Controllers
[HttpPost("register")] [HttpPost("register")]
[AllowAnonymous] [AllowAnonymous]
[CaptchaProtected]
public async Task PostRegister([FromBody]RegisterRequestModel model) public async Task PostRegister([FromBody]RegisterRequestModel model)
{ {
var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash, var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash,

View File

@ -60,7 +60,7 @@ namespace Bit.Core.IdentityServer
//} //}
string bypassToken = null; string bypassToken = null;
if (_captchaValidationService.ServiceEnabled && (_currentContext.IsBot || _captchaValidationService.RequireCaptcha)) if (_captchaValidationService.RequireCaptchaValidation(_currentContext))
{ {
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant()); var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString(); var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
@ -69,7 +69,7 @@ namespace Bit.Core.IdentityServer
{ {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.", context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
new Dictionary<string, object> { new Dictionary<string, object> {
{ "HCaptcha_SiteKey", _captchaValidationService.SiteKey }, { _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
}); });
return; return;
} }

View File

@ -9,7 +9,7 @@ using Newtonsoft.Json;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
public class RegisterRequestModel : IValidatableObject public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
{ {
[StringLength(50)] [StringLength(50)]
public string Name { get; set; } public string Name { get; set; }
@ -22,6 +22,7 @@ namespace Bit.Core.Models.Api
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
[StringLength(50)] [StringLength(50)]
public string MasterPasswordHint { get; set; } public string MasterPasswordHint { get; set; }
public string CaptchaResponse { get; set; }
public string Key { get; set; } public string Key { get; set; }
public KeysRequestModel Keys { get; set; } public KeysRequestModel Keys { get; set; }
public string Token { get; set; } public string Token { get; set; }

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Api
{
public interface ICaptchaProtectedModel
{
string CaptchaResponse { get; set; }
}
}

View File

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

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Context;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -33,9 +34,8 @@ namespace Bit.Core.Services
_dataProtector = dataProtectorProvider.CreateProtector("CaptchaServiceDataProtector"); _dataProtector = dataProtectorProvider.CreateProtector("CaptchaServiceDataProtector");
} }
public bool ServiceEnabled => true; public string SiteKeyResponseKeyName => "HCaptcha_SiteKey";
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey; public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
public bool RequireCaptcha => _globalSettings.Captcha.RequireCaptcha;
public string GenerateCaptchaBypassToken(User user) => public string GenerateCaptchaBypassToken(User user) =>
$"{TokenClearTextPrefix}{_dataProtector.Protect(CaptchaBypassTokenContent(user))}"; $"{TokenClearTextPrefix}{_dataProtector.Protect(CaptchaBypassTokenContent(user))}";
@ -44,9 +44,9 @@ namespace Bit.Core.Services
CoreHelpers.TokenIsValid(TokenName, _dataProtector, encryptedToken[TokenClearTextPrefix.Length..], CoreHelpers.TokenIsValid(TokenName, _dataProtector, encryptedToken[TokenClearTextPrefix.Length..],
user.Email, user.Id, TokenLifetimeInHours); user.Email, user.Id, TokenLifetimeInHours);
public async Task<bool> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress) public async Task<bool> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress)
{ {
if (string.IsNullOrWhiteSpace(captchResponse)) if (string.IsNullOrWhiteSpace(captchaResponse))
{ {
return false; return false;
} }
@ -59,7 +59,7 @@ namespace Bit.Core.Services
RequestUri = new Uri("https://hcaptcha.com/siteverify"), RequestUri = new Uri("https://hcaptcha.com/siteverify"),
Content = new FormUrlEncodedContent(new Dictionary<string, string> Content = new FormUrlEncodedContent(new Dictionary<string, string>
{ {
{ "response", captchResponse.TrimStart("hcaptcha|".ToCharArray()) }, { "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) },
{ "secret", _globalSettings.Captcha.HCaptchaSecretKey }, { "secret", _globalSettings.Captcha.HCaptchaSecretKey },
{ "sitekey", SiteKey }, { "sitekey", SiteKey },
{ "remoteip", clientIpAddress } { "remoteip", clientIpAddress }
@ -87,6 +87,9 @@ namespace Bit.Core.Services
return (bool)jsonResponse.success; return (bool)jsonResponse.success;
} }
public bool RequireCaptchaValidation(ICurrentContext currentContext) =>
currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
private static string CaptchaBypassTokenContent(User user) => private static string CaptchaBypassTokenContent(User user) =>
string.Join(' ', new object[] { string.Join(' ', new object[] {
TokenName, TokenName,

View File

@ -1,13 +1,14 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Context;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class NoopCaptchaValidationService : ICaptchaValidationService public class NoopCaptchaValidationService : ICaptchaValidationService
{ {
public bool ServiceEnabled => false; public string SiteKeyResponseKeyName => null;
public string SiteKey => null; public string SiteKey => null;
public bool RequireCaptcha => false; public bool RequireCaptchaValidation(ICurrentContext currentContext) => false;
public string GenerateCaptchaBypassToken(User user) => ""; public string GenerateCaptchaBypassToken(User user) => "";
public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false; public bool ValidateCaptchaBypassToken(string encryptedToken, User user) => false;

View File

@ -473,7 +473,7 @@ namespace Bit.Core.Settings
public class CaptchaSettings public class CaptchaSettings
{ {
public bool RequireCaptcha { get; set; } = false; public bool ForceCaptchaRequired { get; set; } = false;
public string HCaptchaSecretKey { get; set; } public string HCaptchaSecretKey { get; set; }
public string HCaptchaSiteKey { get; set; } public string HCaptchaSiteKey { get; set; }
} }

View File

@ -0,0 +1,37 @@
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Models.Api;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Utilities
{
public class CaptchaProtectedAttribute : ActionFilterAttribute
{
public string ModelParameterName { get; set; } = "model";
public override void OnActionExecuting(ActionExecutingContext context)
{
var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();
var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService<ICaptchaValidationService>();
if (captchaValidationService.RequireCaptchaValidation(currentContext))
{
var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse;
if (string.IsNullOrWhiteSpace(captchaResponse))
{
throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey);
}
var captchaValid = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
currentContext.IpAddress).GetAwaiter().GetResult();
if (!captchaValid)
{
throw new BadRequestException("Captcha is invalid. Please refresh and try again");
}
}
}
}
}