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:
parent
46fa6f6673
commit
7a135ae7cd
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
7
src/Core/Models/Api/Request/ICaptchaProtectedModel.cs
Normal file
7
src/Core/Models/Api/Request/ICaptchaProtectedModel.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Models.Api
|
||||||
|
{
|
||||||
|
public interface ICaptchaProtectedModel
|
||||||
|
{
|
||||||
|
string CaptchaResponse { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
37
src/Core/Utilities/CaptchaProtectedAttribute.cs
Normal file
37
src/Core/Utilities/CaptchaProtectedAttribute.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user