Skip to content

Commit

Permalink
Merge pull request #8 from jgdevlabs/dev
Browse files Browse the repository at this point in the history
Adding full support for reCAPTCHA V3 and automatic binding to challenges
  • Loading branch information
jooni91 committed Apr 22, 2022
2 parents 105b5b1 + 84c31d8 commit 90f258e
Show file tree
Hide file tree
Showing 28 changed files with 1,228 additions and 840 deletions.
Empty file added CHANGELOG.md
Empty file.
File renamed without changes.
9 changes: 7 additions & 2 deletions ReCaptcha.sln
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29728.190
# Visual Studio Version 17
VisualStudioVersion = 17.1.32210.238
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReCaptcha", "src\ReCaptcha\ReCaptcha.csproj", "{0553A2AB-29DC-4328-9F26-3537597C3C23}"
EndProject
Expand All @@ -10,6 +10,11 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3A037F16-5150-43AA-80BD-872D99F3F94B}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
azure-pipelines.yml = azure-pipelines.yml
CHANGELOG.md = CHANGELOG.md
LICENSE.md = LICENSE.md
pr-pipelines.yml = pr-pipelines.yml
README.md = README.md
EndProjectSection
EndProject
Global
Expand Down
3 changes: 3 additions & 0 deletions azure-pipelines.yml
Expand Up @@ -6,6 +6,9 @@ trigger:
paths:
exclude:
- '*/README.md'
- '*/.github/*'
- '*/LICENSE.md'
- '*/CHANGELOG.md'

pr: none

Expand Down
523 changes: 0 additions & 523 deletions docs/GSoftware.AspNetCore.ReCaptcha.xml

This file was deleted.

215 changes: 129 additions & 86 deletions docs/Griesoft.AspNetCore.ReCaptcha.xml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/ReCaptcha/BackwardsCompatibility/NullableAttributes.cs
@@ -1,5 +1,5 @@
#define INTERNAL_NULLABLE_ATTRIBUTES
#if !NETCOREAPP3_0
#if NET461

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
Expand Down
1 change: 1 addition & 0 deletions src/ReCaptcha/Configuration/RecaptchaServiceConstants.cs
Expand Up @@ -7,6 +7,7 @@ internal class RecaptchaServiceConstants
{
internal const string GoogleRecaptchaEndpoint = "https://www.google.com/recaptcha/api/siteverify";
internal const string TokenKeyName = "G-Recaptcha-Response";
internal const string TokenKeyNameLower = "g-recaptcha-response";
internal const string SettingsSectionKey = "RecaptchaSettings";
}
}
16 changes: 9 additions & 7 deletions src/ReCaptcha/Enums/Render.cs
@@ -1,23 +1,25 @@
namespace Griesoft.AspNetCore.ReCaptcha.TagHelpers
using System;

namespace Griesoft.AspNetCore.ReCaptcha.TagHelpers
{
/// <summary>
/// Recaptcha rendering options for the <see cref="RecaptchaScriptTagHelper"/>.
/// </summary>
[Flags]
public enum Render
{
/// <summary>
/// Render the reCAPTCHA elements explicitly after the scripts have loaded successfully.
/// You need to provide a success callback and handle rendering of the reCAPTCHA elements in it yourself.
/// The default rendering option. This will render your V2 reCAPTCHA elements automatically after the script has been loaded.
/// </summary>
Explicit,
Onload,

/// <summary>
/// The default rendering option. This will render your reCAPTCHA elements automatically after the script has loaded successfully.
/// When rendering your reCAPTCHA elements explicitly a given onloadCallback will be called after the script has been loaded.
/// </summary>
Onload,
Explicit,

/// <summary>
/// Loads the reCAPTCHA V3 script. It will behave mostly the same like <see cref="Explicit"/>, so you have to render the reCAPTCHA elements yourself.
/// Loads the reCAPTCHA V3 script.
/// </summary>
V3
}
Expand Down
1 change: 0 additions & 1 deletion src/ReCaptcha/Extensions/TagHelperOutputExtensions.cs
Expand Up @@ -18,7 +18,6 @@ internal static class TagHelperOutputExtensions
{
private static readonly char[] SpaceChars = { '\u0020', '\u0009', '\u000A', '\u000C', '\u000D' };

[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "<Pending>")]
internal static string AddQueryString(string uri, IEnumerable<KeyValuePair<string, string>> queryString)
{
if (uri == null)
Expand Down
44 changes: 23 additions & 21 deletions src/ReCaptcha/Filters/ValidateRecaptchaFilter.cs
Expand Up @@ -30,6 +30,8 @@ internal class ValidateRecaptchaFilter : IAsyncActionFilter

public ValidationFailedAction OnValidationFailedAction { get; set; } = ValidationFailedAction.Unspecified;

public string? Action { get; set; }

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (OnValidationFailedAction == ValidationFailedAction.Unspecified)
Expand Down Expand Up @@ -71,19 +73,34 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
context.HttpContext.Connection.RemoteIpAddress.ToString() :
null;
}
private bool TryGetRecaptchaToken(HttpRequest request, [NotNullWhen(true)] out string? token)
private bool ShouldShortCircuit(ActionExecutingContext context, ValidationResponse response)
{
if (!response.Success || Action != response.Action)
{
_logger.LogInformation(Resources.InvalidResponseTokenMessage);

if (OnValidationFailedAction == ValidationFailedAction.BlockRequest)
{
context.Result = new RecaptchaValidationFailedResult();
return true;
}
}

return false;
}
private static bool TryGetRecaptchaToken(HttpRequest request, [NotNullWhen(true)] out string? token)
{
if (request.Headers.ContainsKey(RecaptchaServiceConstants.TokenKeyName))
{
token = request.Headers[RecaptchaServiceConstants.TokenKeyName];
}
else if (request.HasFormContentType && request.Form.ContainsKey(RecaptchaServiceConstants.TokenKeyName.ToLowerInvariant()))
else if (request.HasFormContentType && request.Form.ContainsKey(RecaptchaServiceConstants.TokenKeyNameLower))
{
token = request.Form[RecaptchaServiceConstants.TokenKeyName.ToLowerInvariant()];
token = request.Form[RecaptchaServiceConstants.TokenKeyNameLower];
}
else if (request.Query.ContainsKey(RecaptchaServiceConstants.TokenKeyName.ToLowerInvariant()))
else if (request.Query.ContainsKey(RecaptchaServiceConstants.TokenKeyNameLower))
{
token = request.Query[RecaptchaServiceConstants.TokenKeyName.ToLowerInvariant()];
token = request.Query[RecaptchaServiceConstants.TokenKeyNameLower];
}
else
{
Expand All @@ -92,22 +109,7 @@ private bool TryGetRecaptchaToken(HttpRequest request, [NotNullWhen(true)] out s

return token != null;
}
private bool ShouldShortCircuit(ActionExecutingContext context, ValidationResponse response)
{
if (!response.Success)
{
_logger.LogInformation(Resources.InvalidResponseTokenMessage);

if (OnValidationFailedAction == ValidationFailedAction.BlockRequest)
{
context.Result = new RecaptchaValidationFailedResult();
return true;
}
}

return false;
}
private void TryAddResponseToActionAguments(ActionExecutingContext context, ValidationResponse response)
private static void TryAddResponseToActionAguments(ActionExecutingContext context, ValidationResponse response)
{
if (context.ActionArguments.Any(pair => pair.Value is ValidationResponse))
{
Expand Down
11 changes: 10 additions & 1 deletion src/ReCaptcha/Localization/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/ReCaptcha/Localization/Resources.resx
Expand Up @@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ActionPropertyNullErrorMessage" xml:space="preserve">
<value>The action is a madatory property, but was not set. </value>
</data>
<data name="CallbackPropertyNullErrorMessage" xml:space="preserve">
<value>A callback function name must be specified. Invisible reCAPTCHA does not work without it.</value>
</data>
Expand Down
2 changes: 1 addition & 1 deletion src/ReCaptcha/Models/ValidationResponse.cs
Expand Up @@ -42,7 +42,7 @@ public class ValidationResponse
public string Hostname { get; set; } = string.Empty;

/// <summary>
/// List of <see cref="ValidationError"/>'s, if any occured.
/// List of <see cref="ValidationError"/>'s, if any occurred.
/// </summary>
[JsonIgnore]
public IEnumerable<ValidationError> Errors => GetValidationErrors();
Expand Down
120 changes: 55 additions & 65 deletions src/ReCaptcha/ReCaptcha.csproj
@@ -1,69 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp3.0;net461</TargetFrameworks>
<RootNamespace>Griesoft.AspNetCore.ReCaptcha</RootNamespace>
<AssemblyName>Griesoft.AspNetCore.ReCaptcha</AssemblyName>
<Company>Griesinger Software</Company>
<Authors>Joonas Griesinger</Authors>
<Owners>jgdevlabs,jooni91</Owners>
<Title>ASP.NET Core reCAPTCHA Service</Title>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
<Description>A Google reCPATCHA validation wrapper service for ASP.NET Core. With only a few simple setup steps you are ready to block bots from filling in and submitting forms on your website.</Description>
<Copyright>2020 © Griesinger Software</Copyright>
<PackageProjectUrl>https://github.com/jgdevlabs/aspnetcore-recaptcha</PackageProjectUrl>
<RepositoryUrl>https://github.com/jgdevlabs/aspnetcore-recaptcha</RepositoryUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageTags>aspnetcore;recaptcha;aspnetcoremvc;actionfilter;google;razor;recaptcha-v2;recaptcha-v3;taghelper</PackageTags>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>D:\repos\ReCaptcha\docs\Griesoft.AspNetCore.ReCaptcha.xml</DocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.1.0" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Update="Localization\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>

<ItemGroup>
<Compile Update="Localization\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>

<PropertyGroup Condition="'$(TargetFramework)' != 'netcoreapp3.0'">
<NoWarn>$(NoWarn);8600;8601;8602;8603;8604</NoWarn>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net461;net5.0;net6.0</TargetFrameworks>
<RootNamespace>Griesoft.AspNetCore.ReCaptcha</RootNamespace>
<AssemblyName>Griesoft.AspNetCore.ReCaptcha</AssemblyName>
<Company>Griesinger Software</Company>
<Authors>Joonas Griesinger</Authors>
<Owners>jgdevlabs,jooni91</Owners>
<Title>ASP.NET Core reCAPTCHA Service</Title>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Description>A Google reCAPTCHA validation wrapper service for ASP.NET Core. With only a few simple setup steps you are ready to block bots from filling in and submitting forms on your website.</Description>
<Copyright>2022 © Griesinger Software</Copyright>
<PackageProjectUrl>https://github.com/jgdevlabs/aspnetcore-recaptcha</PackageProjectUrl>
<RepositoryUrl>https://github.com/jgdevlabs/aspnetcore-recaptcha</RepositoryUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageTags>aspnetcore;recaptcha;aspnetcoremvc;recaptcha-v2;recaptcha-v3</PackageTags>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>..\..\docs\Griesoft.AspNetCore.ReCaptcha.xml</DocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Update="Localization\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>

<ItemGroup>
<Compile Update="Localization\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>

</Project>
42 changes: 42 additions & 0 deletions src/ReCaptcha/TagHelpers/CallbackScriptTagHelperComponent.cs
@@ -0,0 +1,42 @@
using System;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Razor.TagHelpers;

[assembly: InternalsVisibleTo("ReCaptcha.Tests")]
namespace Griesoft.AspNetCore.ReCaptcha.TagHelpers
{
/// <summary>
/// This tag helper component is used to add a short callback script to the bottom of a body tag.
/// </summary>
/// <remarks>
/// The callback script is used as a default callback function to submit a form after a reCAPTCHA challenge was successful.
/// </remarks>
internal class CallbackScriptTagHelperComponent : TagHelperComponent
{
private readonly string _formId;

public CallbackScriptTagHelperComponent(string formId)
{
if (string.IsNullOrEmpty(formId))
{
throw new ArgumentNullException(nameof(formId));
}

_formId = formId;
}

public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (string.Equals(context.TagName, "body", StringComparison.OrdinalIgnoreCase))
{
output.PostContent.AppendHtml(CallbackScript(_formId));
}
}

public static string CallbackScript(string formId)
{
// Append the formId to the function name in case that multiple recaptcha tags are added in a document.
return $"<script>function submit{formId}(token){{document.getElementById('{formId}').submit();}}</script>";
}
}
}

0 comments on commit 90f258e

Please sign in to comment.