Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract Captcha logic and robot detection to an abstractions library #5932

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.ReCaptcha", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.ReCaptcha.Core", "src\OrchardCore\OrchardCore.ReCaptcha.Core\OrchardCore.ReCaptcha.Core.csproj", "{FBF1CA0B-A800-424B-B636-262B15850E44}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Captcha.Abstractions", "src\OrchardCore\OrchardCore.Captcha.Abstractions\OrchardCore.Captcha.Abstractions.csproj", "{4DF4663D-F0E2-4CB1-B4E8-0DAB052C7660}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Apis.GraphQL", "src\OrchardCore.Modules\OrchardCore.Apis.GraphQL\OrchardCore.Apis.GraphQL.csproj", "{A1622483-2523-4A31-8164-2D81A6619FEE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Apis.GraphQL.Abstractions", "src\OrchardCore\OrchardCore.Apis.GraphQL.Abstractions\OrchardCore.Apis.GraphQL.Abstractions.csproj", "{A0B71FF5-2E4D-4B17-99D9-B3C2C1C593E6}"
Expand Down Expand Up @@ -878,6 +880,10 @@ Global
{FBF1CA0B-A800-424B-B636-262B15850E44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBF1CA0B-A800-424B-B636-262B15850E44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBF1CA0B-A800-424B-B636-262B15850E44}.Release|Any CPU.Build.0 = Release|Any CPU
{4DF4663D-F0E2-4CB1-B4E8-0DAB052C7660}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DF4663D-F0E2-4CB1-B4E8-0DAB052C7660}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DF4663D-F0E2-4CB1-B4E8-0DAB052C7660}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DF4663D-F0E2-4CB1-B4E8-0DAB052C7660}.Release|Any CPU.Build.0 = Release|Any CPU
{A1622483-2523-4A31-8164-2D81A6619FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1622483-2523-4A31-8164-2D81A6619FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1622483-2523-4A31-8164-2D81A6619FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -1121,6 +1127,7 @@ Global
{927ABC87-A7B8-40C4-B705-86C88C807C92} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{8198F638-C167-41F9-BDEA-DBE5BC21CAFB} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{FBF1CA0B-A800-424B-B636-262B15850E44} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{4DF4663D-F0E2-4CB1-B4E8-0DAB052C7660} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{A1622483-2523-4A31-8164-2D81A6619FEE} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{A0B71FF5-2E4D-4B17-99D9-B3C2C1C593E6} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{B0FA3A30-E3AE-470A-9102-9EB107482254} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace OrchardCore.ReCaptcha.ActionFilters
namespace OrchardCore.Captcha.ActionFilters
{
public enum ReCaptchaMode
public enum CaptchaMode
{
/// <summary>
/// Captcha is always shown
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace OrchardCore.ReCaptcha.ActionFilters.Detection
namespace OrchardCore.Captcha.ActionFilters.Detection
{
/// <summary>
/// This interface describes the contract for components that can detect Robots.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using OrchardCore.ReCaptcha.Configuration;
using OrchardCore.Captcha.Configuration;

namespace OrchardCore.ReCaptcha.ActionFilters.Detection
namespace OrchardCore.Captcha.ActionFilters.Detection
{
public class IpAddressRobotDetector : IDetectRobots
{
private const string IpAddressAbuseDetectorCacheKey = "IpAddressRobotDetector";

private readonly IMemoryCache _memoryCache;
private readonly HttpContext _httpContext;
private readonly ReCaptchaSettings _settings;
private readonly CaptchaSettings _settings;

public IpAddressRobotDetector(IHttpContextAccessor httpContextAccessor, IMemoryCache memoryCache, IOptions<ReCaptchaSettings> settingsAccessor)
public IpAddressRobotDetector(IHttpContextAccessor httpContextAccessor, IMemoryCache memoryCache, IOptions<CaptchaSettings> settingsAccessor)
{
_httpContext = httpContextAccessor.HttpContext;
_memoryCache = memoryCache;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using OrchardCore.Captcha.Services;

namespace OrchardCore.Captcha.ActionFilters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateCaptchaAttribute : ActionFilterAttribute
{
private readonly CaptchaMode _mode;

public ValidateCaptchaAttribute(CaptchaMode mode = CaptchaMode.AlwaysShow)
{
_mode = mode;
}

public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
var captchaService = context.HttpContext.RequestServices.GetService<CaptchaService>();
var isValidCaptcha = false;
var isRobot = false;

switch (_mode)
{
case CaptchaMode.PreventRobots:
isRobot = captchaService.IsThisARobot();
break;
case CaptchaMode.AlwaysShow:
isRobot = true;
break;
}

if (isRobot)
{
isValidCaptcha= await captchaService.ValidateCaptchaAsync((key, error) =>
{
context.ModelState.AddModelError(key, error);
});
}

await next();

if (isValidCaptcha && context.ModelState.IsValid)
{
captchaService.ThisIsAHuman();
}
else
{
captchaService.MaybeThisIsARobot();
}

return;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace OrchardCore.Captcha.Configuration
{
public class CaptchaSettings
{
/// <summary>
/// Value for supplying the amount of lenience we are willing to show robots
/// </summary>
public int DetectionThreshold { get; set; } = 5;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(AspNetCoreTargetFramework)</TargetFramework>
<RootNamespace>OrchardCore.Captcha</RootNamespace>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OrchardCore.Modules;
using OrchardCore.Captcha.ActionFilters.Detection;

namespace OrchardCore.Captcha.Services
{
public abstract class CaptchaService
{
private readonly IEnumerable<IDetectRobots> _robotDetectors;
private readonly ILogger _logger;
public CaptchaService(IEnumerable<IDetectRobots> robotDetectors,ILogger logger)
{
_robotDetectors = robotDetectors;
_logger = logger;
}

/// <summary>
/// Flags the behavior as that of a robot
/// </summary>
public virtual void MaybeThisIsARobot()
{
_robotDetectors.Invoke(i => i.FlagAsRobot(), _logger);
}

/// <summary>
/// Determines if the request has been made by a robot
/// </summary>
/// <returns>Yes (true) or no (false)</returns>
public virtual bool IsThisARobot()
{
var result = _robotDetectors.Invoke(i => i.DetectRobot(), _logger);
return result.Any(a => a.IsRobot);
}

/// <summary>
/// Clears all robot markers, we are dealing with a human
/// </summary>
/// <returns></returns>
public virtual void ThisIsAHuman()
{
_robotDetectors.Invoke(i => i.IsNotARobot(), _logger);
}

/// <summary>
/// Verifies the captcha response
/// </summary>
/// <param name="captchaResponse"></param>
/// <returns></returns>
public abstract Task<bool> VerifyCaptchaResponseAsync(string captchaResponse);

/// <summary>
/// Validates the captcha that is in the Form of the current request
/// </summary>
/// <param name="reportError">Lambda for reporting errors</param>
public abstract Task<bool> ValidateCaptchaAsync(Action<string, string> reportError);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Modules;
using OrchardCore.Captcha.ActionFilters;
using OrchardCore.Captcha.ActionFilters.Detection;
using OrchardCore.Captcha.Configuration;

namespace OrchardCore.Captcha.TagHelpers
{
public abstract class CaptchaTagHelper : TagHelper
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly CaptchaSettings _settings;
private readonly ILogger _logger;

public CaptchaTagHelper(IOptions<CaptchaSettings> optionsAccessor, IHttpContextAccessor httpContextAccessor, ILogger logger)
{
_httpContextAccessor = httpContextAccessor;
_settings = optionsAccessor.Value;
Mode = CaptchaMode.PreventRobots;
_logger = logger;
}

public virtual CaptchaMode Mode { get; set; }

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var captchaRequired = false;
var isConfigured = _settings != null;

if(isConfigured)
{
if (Mode == CaptchaMode.AlwaysShow)
{
captchaRequired = true;
}
else if (Mode == CaptchaMode.PreventRobots)
{
var robotDetectors = _httpContextAccessor.HttpContext.RequestServices.GetServices<IDetectRobots>();
var robotDetected = robotDetectors.Invoke(d => d.DetectRobot(), _logger).Any(d => d.IsRobot);

if (robotDetected)
{
captchaRequired = true;
}
}
}

if (captchaRequired)
{
await ShowCaptcha(output);
}
else
{
output.SuppressOutput();
}

}

protected abstract Task ShowCaptcha(TagHelperOutput output);

}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using OrchardCore.Captcha.Configuration;

namespace OrchardCore.ReCaptcha.Configuration
{
public class ReCaptchaSettings
public class ReCaptchaSettings : CaptchaSettings
{
public string SiteKey { get; set; }

Expand All @@ -10,11 +12,6 @@ public class ReCaptchaSettings

public string ReCaptchaApiUri { get; set; } = Constants.ReCaptchaApiUri;

/// <summary>
/// Value for supplying the amount of lenience we are willing to show robots
/// </summary>
public int DetectionThreshold { get; set; } = 5;

public bool IsValid()
{
return !string.IsNullOrWhiteSpace(SiteKey) && !string.IsNullOrWhiteSpace(SecretKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\OrchardCore.Captcha.Abstractions\OrchardCore.Captcha.Abstractions.csproj" />
<ProjectReference Include="..\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" />
<ProjectReference Include="..\OrchardCore.ResourceManagement.Abstractions\OrchardCore.ResourceManagement.Abstractions.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.ReCaptcha.ActionFilters.Detection;
using OrchardCore.Captcha.ActionFilters.Detection;
using OrchardCore.Captcha.Services;
using OrchardCore.ReCaptcha.Configuration;
using OrchardCore.ReCaptcha.Services;
using Polly;
Expand All @@ -17,6 +18,7 @@ public static IServiceCollection AddReCaptcha(this IServiceCollection services,

services.AddTransient<IDetectRobots, IpAddressRobotDetector>();
services.AddTransient<IConfigureOptions<ReCaptchaSettings>, ReCaptchaSettingsConfiguration>();
services.AddTransient<CaptchaService, ReCaptchaService>();
services.AddTransient<ReCaptchaService>();

if (configure != null)
Expand Down
Loading