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

Adding full support for reCAPTCHA V3 and automatic binding to challenges #8

Merged
merged 18 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7e5a4f4
Dropping support for netcoreapp2.1.
jooni91 Apr 12, 2022
2b208e9
Upgrading the test project to net6.0.
jooni91 Apr 12, 2022
675bfcb
Adding a few project files to Solution Items for easy access in VS.
jooni91 Apr 12, 2022
837b727
Just doing small code improvements, cleanup and typo fixes.
jooni91 Apr 12, 2022
ae84c7b
Updating project meta data. Also merging dependencies of all target f…
jooni91 Apr 18, 2022
ca3d8a3
Adding a default callback script in the form of a TagHelperComponent …
jooni91 Apr 19, 2022
18df1ac
Changing access to the CallbackScriptTagHelperComponent to internal a…
jooni91 Apr 19, 2022
943f02d
Small fixes to the docs.
jooni91 Apr 19, 2022
277b5bc
Adding tests for the CallbackScriptTagHelperComponent.
jooni91 Apr 19, 2022
d5fef5d
Adding a reCAPTCHA V3 tag. This can be used to automatically bind the…
jooni91 Apr 20, 2022
43c267f
Updating the documentations of the tag helper classes.
jooni91 Apr 20, 2022
c771098
Adding now also support for automatic binding of the challenge in our…
jooni91 Apr 20, 2022
d9e9ee7
Remove some old documentation file and add the documentation to the b…
jooni91 Apr 20, 2022
d2e566c
Include symbols and xml documentation in the build output.
jooni91 Apr 20, 2022
67c3a78
Had a small error in the FormId attribute format which does translate…
jooni91 Apr 20, 2022
eebb309
Devs who make use of reCAPTCHA V3 can / should now specify an action …
jooni91 Apr 20, 2022
6398edf
Added a change log markdown file to the repo.
jooni91 Apr 22, 2022
84c31d8
Merge branch 'master' into dev
jooni91 Apr 22, 2022
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
Empty file added CHANGELOG.md
Empty file.
File renamed without changes.
9 changes: 7 additions & 2 deletions ReCaptcha.sln
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public ValidateRecaptchaFilter(IRecaptchaService recaptchaService, IOptionsMonit

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>";
}
}
}
Loading