Skip to content

Commit 7aa8212

Browse files
authored
Merge pull request #9 from jgdevlabs/dev
Performance improvements and meta data changes
2 parents a9a2294 + d98399c commit 7aa8212

15 files changed

+177
-59
lines changed

README.md

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# ASP.NET Core reCAPTCHA
2-
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.
1+
# ASP.NET Core reCAPTCHA
2+
A Google reCAPTCHA validation wrapper service for ASP.NET Core. In only a few simple steps, you are ready to block bots from filling in and submitting forms on your website with reCAPTCHA.
33

4-
This package also supports reCAPTCHA V3, but at the moment does not provide any frontend tag helpers for that. So only backend validation is supported at the moment.
4+
The package supports V2 and V3 and comes with tag helpers that make it easy to add challenges to your forms. Also, backend validation is made easy and requires only the use of an attribute in your controllers or actions that should get validated.
55

66
[![Build Status](https://dev.azure.com/griesingersoftware/ASP.NET%20Core%20Recaptcha/_apis/build/status/jgdevlabs.aspnetcore-recaptcha?branchName=master)](https://dev.azure.com/griesingersoftware/ASP.NET%20Core%20Recaptcha/_build/latest?definitionId=17&branchName=master)
77
[![Build Status](https://vsrm.dev.azure.com/griesingersoftware/_apis/public/Release/badge/f9036ec9-eb1c-4aff-a2b8-27fdaa573d0f/1/2)](https://vsrm.dev.azure.com/griesingersoftware/_apis/public/Release/badge/f9036ec9-eb1c-4aff-a2b8-27fdaa573d0f/1/2)
@@ -13,16 +13,19 @@ This package also supports reCAPTCHA V3, but at the moment does not provide any
1313

1414
Install via [NuGet](https://www.nuget.org/packages/Griesoft.AspNetCore.ReCaptcha/) using:
1515

16-
``PM> Install-Package Griesoft.AspNetCore.ReCaptcha``
16+
`PM> Install-Package Griesoft.AspNetCore.ReCaptcha`
1717

1818
## Quickstart
1919

20-
The first thing you need to do is to sign up for a new API key-pair for your project. You can follow [Google's guide](https://developers.google.com/recaptcha/intro#overview), if you haven't done that yet.
20+
### Prequisites
21+
You will need an API key pair which can be acquired by [signing up here](http://www.google.com/recaptcha/admin). For assistance or other questions regarding that topic, refer to [Google's guide](https://developers.google.com/recaptcha/intro#overview).
2122

22-
After sign-up you should now have a **Site key** and a **Secret key**. Make note of those, you will need them for the next step.
23+
After sign-up, you should have a **Site key** and a **Secret key**. You will need those to configure the service in your app.
2324

2425
### Configuration
2526

27+
#### Settings
28+
2629
Open your `appsettings.json` and add the following lines:
2730

2831
```json
@@ -31,41 +34,57 @@ Open your `appsettings.json` and add the following lines:
3134
"SecretKey": "<Your secret key goes here>"
3235
}
3336
```
37+
**Important:** The `SiteKey` will be exposed to the public, so make sure you don't accidentally swap it with the `SecretKey`.
3438

35-
Make sure to place your site & secret key in the right spot. You risk to expose your secret key to the public if you switch it with the site key.
39+
#### Service Registration
3640

37-
For more inforamtion about ASP.NET Core configuration check out the [Microsoft docs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1).
41+
Register this service by calling the `AddRecaptchaService()` method which is an extension method of `IServiceCollection`. For example:
3842

39-
In your `Startup.cs` you now need to add the service. Add the following line `services.AddRecaptchaService();` into the `ConfigureServices(IServiceCollection services)` method, like this for excample.
43+
##### .NET 6
44+
45+
```csharp
46+
var builder = WebApplication.CreateBuilder(args);
47+
48+
builder.Services.AddRecaptchaService();
49+
```
50+
51+
##### Prior to .NET 6
4052

4153
```csharp
4254
public void ConfigureServices(IServiceCollection services)
4355
{
44-
// Add other services here
45-
4656
services.AddRecaptchaService();
47-
48-
// Add other services here
4957
}
5058
```
5159

52-
### Adding a reCAPTCHA element on your view
60+
### Adding a reCAPTCHA element to your view
5361

54-
First you will need to import the tag helpers. Open your `_ViewImports.cshtml` file and add the following lines:
62+
First, import the tag helpers. Open your `_ViewImports.cshtml` file and add the following lines:
5563

5664
```razor
5765
@using Griesoft.AspNetCore.ReCaptcha
58-
5966
@addTagHelper *, Griesoft.AspNetCore.ReCaptcha
6067
```
6168

62-
Now you are ready to use the tag helpers in your views. Always add the `<recaptcha-script>` tag on the bottom of your view. This will render the script tag which will load the reCAPTCHA.js API.
69+
Next, you need to add the `<recaptcha-script>` to every view you intend to use the reCAPTCHA. That will render the API script. Preferably you would add this somewhere close to the bottom of your body element.
6370

64-
Next you only need to add a `<recaptcha>` tag in your form and you are all set. This is the most simplest way of adding reCAPTCHA to your views. Now you only need to add backend validation to the controller of your view.
71+
Now you may add a reCAPTCHA challenge to your view where ever you need it. Using the `<recaptcha />` tag in your form will render a reCAPTCHA V2 checkbox inside it.
72+
73+
For invisible reCAPTCHA use:
74+
```html
75+
<button re-invisible form-id="yourFormId">Submit</button>
76+
```
77+
78+
For reCAPTCHA V3 use:
79+
```html
80+
<recaptcha-v3 form-id="yourFormId" action="submit">Submit</recaptcha-v3>
81+
```
6582

6683
### Adding backend validation to an action
6784

68-
Add a using statement to `Griesoft.AspNetCore.ReCaptcha` in your controller. Next you just need to the `[ValidateRecaptcha]` attribute to the action which is triggered by your form.
85+
Validation is done by decorating your controller or action with `[ValidateRecaptcha]`.
86+
87+
For example:
6988

7089
```csharp
7190
using Griesoft.AspNetCore.ReCaptcha;
@@ -75,25 +94,54 @@ namespace ReCaptcha.Sample.Controllers
7594
{
7695
public class ExampleController : Controller
7796
{
78-
public IActionResult Index()
79-
{
80-
return View();
81-
}
82-
8397
[ValidateRecaptcha]
8498
public IActionResult FormSubmit(SomeModel model)
8599
{
86-
// Will hit the next line only if validation was successfull
100+
// Will hit the next line only if validation was successful
87101
return View("FormSubmitSuccessView");
88102
}
89103
}
90104
}
91105
```
106+
Now each incoming request to that action will be validated for a valid reCAPTCHA token.
107+
108+
The default behavior for invalid tokens is a 404 (BadRequest) response. But this behavior is configurable, and you may also instead request the validation result as an argument to your action.
109+
110+
This can be achieved like this:
111+
112+
```csharp
113+
[ValidateRecaptcha(ValidationFailedAction = ValidationFailedAction.ContinueRequest)]
114+
public IActionResult FormSubmit(SomeModel model, ValidationResponse recaptchaResponse)
115+
{
116+
if (!recaptchaResponse.Success)
117+
{
118+
return BadRequest();
119+
}
120+
121+
return View("FormSubmitSuccessView");
122+
}
123+
```
124+
125+
In case you are validating a reCAPTCHA V3 token, make sure you also add an action name to your validator.
126+
127+
For example:
128+
129+
```csharp
130+
[ValidateRecaptcha(Action = "submit")]
131+
public IActionResult FormSubmit(SomeModel model)
132+
{
133+
return View("FormSubmitSuccessView");
134+
}
135+
```
136+
137+
## Options & Customization
92138

93-
Now if validation would fail, the action method would never get called.
139+
There are global defaults that you may modify on your application startup. Also, the appearance and position of V2 tags may be modified. Either globally or each tag individually.
94140

95-
You can configure that behaviour and a lot of other stuff globally at startup or even just seperatly for each controller or action.
141+
All options from the [official reCAPTCHA docs](https://developers.google.com/recaptcha/intro) are available to you in this package.
96142

97-
### Addition information
143+
## Detailed Documentation
144+
Is on it's way...
98145

99-
For more detailed usage guides check out the wiki. You can find guides about additional configuration options, response validation behaviour, explicit rendering of tags, invisible reCAPTCHA elements and the usage of reCAPTCHA V3.
146+
## Contributing
147+
Contributing is heavily encouraged. :muscle: The best way of doing so is by first starting a discussion about new features or improvements you would like to make. Or, in case of a bug, report it first by creating a new issue. From there, you may volunteer to fix it if you like. 😄

ReCaptcha.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1212
.editorconfig = .editorconfig
1313
azure-pipelines.yml = azure-pipelines.yml
1414
CHANGELOG.md = CHANGELOG.md
15+
.github\FUNDING.yml = .github\FUNDING.yml
1516
LICENSE.md = LICENSE.md
1617
pr-pipelines.yml = pr-pipelines.yml
1718
README.md = README.md

docs/Griesoft.AspNetCore.ReCaptcha.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ReCaptcha/Configuration/RecaptchaOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class RecaptchaOptions
1212
/// <summary>
1313
/// If set to true the remote IP will be send to Google when verifying the response token. The default is false.
1414
/// </summary>
15-
public bool UseRemoteIp { get; set; } = false;
15+
public bool UseRemoteIp { get; set; }
1616

1717
/// <summary>
1818
/// Configure the service on a global level whether it should block / short circuit the request pipeline
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using Griesoft.AspNetCore.ReCaptcha.Localization;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace Griesoft.AspNetCore.ReCaptcha.Extensions
6+
{
7+
internal static class LoggerExtensions
8+
{
9+
private static readonly Action<ILogger, Exception?> _validationRequestFailed = LoggerMessage.Define(
10+
LogLevel.Warning,
11+
new EventId(1, nameof(ValidationRequestFailed)),
12+
Resources.RequestFailedErrorMessage);
13+
14+
private static readonly Action<ILogger, Exception?> _validationRequestUnexpectedException = LoggerMessage.Define(
15+
LogLevel.Critical,
16+
new EventId(2, nameof(ValidationRequestUnexpectedException)),
17+
Resources.ValidationUnexpectedErrorMessage);
18+
19+
private static readonly Action<ILogger, Exception?> _recaptchaResponseTokenMissing = LoggerMessage.Define(
20+
LogLevel.Warning,
21+
new EventId(3, nameof(RecaptchaResponseTokenMissing)),
22+
Resources.RecaptchaResponseTokenMissing);
23+
24+
private static readonly Action<ILogger, Exception?> _invalidResponseToken = LoggerMessage.Define(
25+
LogLevel.Information,
26+
new EventId(4, nameof(InvalidResponseToken)),
27+
Resources.InvalidResponseTokenMessage);
28+
29+
public static void ValidationRequestFailed(this ILogger logger)
30+
{
31+
_validationRequestFailed(logger, null);
32+
}
33+
34+
public static void ValidationRequestUnexpectedException(this ILogger logger, Exception exception)
35+
{
36+
_validationRequestUnexpectedException(logger, exception);
37+
}
38+
39+
public static void RecaptchaResponseTokenMissing(this ILogger logger)
40+
{
41+
_recaptchaResponseTokenMissing(logger, null);
42+
}
43+
44+
public static void InvalidResponseToken(this ILogger logger)
45+
{
46+
_invalidResponseToken(logger, null);
47+
}
48+
}
49+
}

src/ReCaptcha/Extensions/TagHelperOutputExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ internal static void AddClass(this TagHelperOutput tagHelperOutput, string class
7272

7373
var encodedSpaceChars = SpaceChars.Where(x => !x.Equals('\u0020')).Select(x => htmlEncoder.Encode(x.ToString(CultureInfo.InvariantCulture))).ToArray();
7474

75-
if (SpaceChars.Any(classValue.Contains) || encodedSpaceChars.Any(value => classValue.IndexOf(value, StringComparison.Ordinal) >= 0))
75+
if (SpaceChars.Any(classValue.Contains) || encodedSpaceChars.Any(value => classValue.Contains(value)))
7676
{
7777
throw new ArgumentException(null, nameof(classValue));
7878
}

src/ReCaptcha/Filters/ValidateRecaptchaFilter.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Runtime.CompilerServices;
55
using System.Threading.Tasks;
66
using Griesoft.AspNetCore.ReCaptcha.Configuration;
7+
using Griesoft.AspNetCore.ReCaptcha.Extensions;
78
using Griesoft.AspNetCore.ReCaptcha.Localization;
89
using Griesoft.AspNetCore.ReCaptcha.Services;
910
using Microsoft.AspNetCore.Http;
@@ -43,7 +44,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
4344

4445
if (!TryGetRecaptchaToken(context.HttpContext.Request, out string? token))
4546
{
46-
_logger.LogWarning(Resources.RecaptchaResponseTokenMissing);
47+
_logger.RecaptchaResponseTokenMissing();
4748

4849
validationResponse = new ValidationResponse()
4950
{
@@ -77,7 +78,7 @@ private bool ShouldShortCircuit(ActionExecutingContext context, ValidationRespon
7778
{
7879
if (!response.Success || Action != response.Action)
7980
{
80-
_logger.LogInformation(Resources.InvalidResponseTokenMessage);
81+
_logger.InvalidResponseToken();
8182

8283
if (OnValidationFailedAction == ValidationFailedAction.BlockRequest)
8384
{

src/ReCaptcha/ReCaptcha.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
2222
<GenerateDocumentationFile>true</GenerateDocumentationFile>
2323
<DocumentationFile>..\..\docs\Griesoft.AspNetCore.ReCaptcha.xml</DocumentationFile>
24+
<PackageReadmeFile>README.md</PackageReadmeFile>
25+
<EnableNETAnalyzers>True</EnableNETAnalyzers>
26+
<AnalysisLevel>latest-recommended</AnalysisLevel>
2427
</PropertyGroup>
2528

2629
<ItemGroup>
@@ -48,6 +51,13 @@
4851
<Folder Include="Properties\" />
4952
</ItemGroup>
5053

54+
<ItemGroup>
55+
<None Include="..\..\README.md">
56+
<Pack>True</Pack>
57+
<PackagePath>\</PackagePath>
58+
</None>
59+
</ItemGroup>
60+
5161
<ItemGroup>
5262
<Compile Update="Localization\Resources.Designer.cs">
5363
<DesignTime>True</DesignTime>

src/ReCaptcha/Services/RecaptchaService.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Runtime.CompilerServices;
55
using System.Threading.Tasks;
66
using Griesoft.AspNetCore.ReCaptcha.Configuration;
7+
using Griesoft.AspNetCore.ReCaptcha.Extensions;
78
using Griesoft.AspNetCore.ReCaptcha.Localization;
89
using Microsoft.Extensions.Logging;
910
using Microsoft.Extensions.Options;
@@ -34,18 +35,26 @@ public async Task<ValidationResponse> ValidateRecaptchaResponse(string token, st
3435

3536
try
3637
{
37-
var response = await _httpClient.PostAsync($"?secret={_settings.SecretKey}&response={token}{(remoteIp != null ? $"&remoteip={remoteIp}" : "")}", null)
38+
var response = await _httpClient.PostAsync($"?secret={_settings.SecretKey}&response={token}{(remoteIp != null ? $"&remoteip={remoteIp}" : "")}", null!)
3839
.ConfigureAwait(true);
3940

4041
response.EnsureSuccessStatusCode();
4142

4243
return JsonConvert.DeserializeObject<ValidationResponse>(
4344
await response.Content.ReadAsStringAsync()
44-
.ConfigureAwait(true));
45+
.ConfigureAwait(true))
46+
?? new ValidationResponse()
47+
{
48+
Success = false,
49+
ErrorMessages = new List<string>()
50+
{
51+
"response-deserialization-failed"
52+
}
53+
};
4554
}
4655
catch (HttpRequestException)
4756
{
48-
_logger.LogWarning(Resources.RequestFailedErrorMessage);
57+
_logger.ValidationRequestFailed();
4958
return new ValidationResponse()
5059
{
5160
Success = false,
@@ -57,7 +66,7 @@ await response.Content.ReadAsStringAsync()
5766
}
5867
catch (Exception ex)
5968
{
60-
_logger.LogCritical(ex, Resources.ValidationUnexpectedErrorMessage);
69+
_logger.ValidationRequestUnexpectedException(ex);
6170
throw;
6271
}
6372
}

src/ReCaptcha/TagHelpers/RecaptchaInvisibleTagHelper.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public RecaptchaInvisibleTagHelper(IOptionsMonitor<RecaptchaSettings> settings,
7474
/// <summary>
7575
/// Set the tabindex of the reCAPTCHA element. If other elements in your page use tabindex, it should be set to make user navigation easier.
7676
/// </summary>
77-
public int? TabIndex { get; set; } = null;
77+
public int? TabIndex { get; set; }
7878

7979
/// <summary>
8080
/// The id of the form that will be submitted after a successful reCAPTCHA challenge.
@@ -86,7 +86,7 @@ public RecaptchaInvisibleTagHelper(IOptionsMonitor<RecaptchaSettings> settings,
8686
/// Set the name of your callback function, which is called when the reCAPTCHA challenge was successful.
8787
/// A "g-recaptcha-response" token is added to your callback function parameters for server-side verification.
8888
/// </summary>
89-
public string Callback { get; set; } = string.Empty;
89+
public string? Callback { get; set; }
9090

9191
/// <summary>
9292
/// Set the name of your callback function, executed when the reCAPTCHA response expires and the user needs to re-verify.
@@ -101,14 +101,14 @@ public RecaptchaInvisibleTagHelper(IOptionsMonitor<RecaptchaSettings> settings,
101101

102102
/// <inheritdoc />
103103
/// <exception cref="ArgumentNullException"></exception>
104-
/// <exception cref="NullReferenceException">Thrown when both <see cref="Callback"/> and <see cref="FormId"/> are null or empty.</exception>
104+
/// <exception cref="InvalidOperationException">Thrown when both <see cref="Callback"/> and <see cref="FormId"/> are null or empty.</exception>
105105
public override void Process(TagHelperContext context, TagHelperOutput output)
106106
{
107107
_ = output ?? throw new ArgumentNullException(nameof(output));
108108

109109
if (string.IsNullOrEmpty(Callback) && string.IsNullOrEmpty(FormId))
110110
{
111-
throw new NullReferenceException(Resources.CallbackPropertyNullErrorMessage);
111+
throw new InvalidOperationException(Resources.CallbackPropertyNullErrorMessage);
112112
}
113113

114114
if (output.TagName == "button")

0 commit comments

Comments
 (0)