Skip to content

Commit

Permalink
#3 - Add reCaptcha server-side v3 documentation and blazor server sample
Browse files Browse the repository at this point in the history
  • Loading branch information
albx committed Dec 2, 2023
1 parent 4df95e5 commit 51c654d
Show file tree
Hide file tree
Showing 14 changed files with 152 additions and 18 deletions.
2 changes: 1 addition & 1 deletion samples/KITT.Web.ReCaptcha.Samples.Http/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
builder.Services.AddCors(
options => options.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));

builder.Services.AddReCaptchaV2(options =>
builder.Services.AddReCaptchaV2HttpClient(options =>
{
options.SecretKey = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"; // this is the v2 test secret
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

builder.Services.AddReCaptchaV2(options =>
builder.Services.AddReCaptchaV2HttpClient(options =>
{
options.SecretKey = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe";
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices((ctx, services) =>
{
services.AddReCaptchaV2(options => options.SecretKey = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe");
services.AddReCaptchaV2HttpClient(options => options.SecretKey = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe");
})
.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\src\KITT.Web.ReCaptcha.Blazor\KITT.Web.ReCaptcha.Blazor.csproj" />
<ProjectReference Include="..\..\..\src\KITT.Web.ReCaptcha.Http\KITT.Web.ReCaptcha.Http.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
@using System.ComponentModel.DataAnnotations
@using KITT.Web.ReCaptcha.Blazor.v3

@inject ReCaptchaService ReCaptcha
@inject Blazor.v3.ReCaptchaService ReCaptchaClient
@inject Http.v3.ReCaptchaService ReCaptchaHttp

<PageTitle>KITT ReCaptcha v3 - Blazor Server sample</PageTitle>

Expand Down Expand Up @@ -45,11 +46,24 @@
{
try
{
var reCaptchaClientResponse = await ReCaptcha.VerifyAsync(action: "submit");
var reCaptchaClientResponse = await ReCaptchaClient.VerifyAsync(action: "submit");
if (reCaptchaClientResponse.Succeeded)
{
message = "reCaptcha validated on client successfully!";
isSuccessMessage = true;
var serverSideResponse = await ReCaptchaHttp.VerifyAsync(
reCaptchaClientResponse.Response,
action: "submit");

if (serverSideResponse.Success)
{
message = "reCaptcha validated successfully!";
isSuccessMessage = true;
}
else
{
message = string.Join(",", serverSideResponse.ErrorCodes);
isSuccessMessage = false;
}

}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using KITT.Web.ReCaptcha.Blazor.v3;
using KITT.Web.ReCaptcha.Http.v3;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

builder.Services.AddReCaptchaV3(options => options.SiteKey = builder.Configuration["ReCaptcha:SiteKey"]!);
builder.Services
.AddReCaptchaV3(options => options.SiteKey = builder.Configuration["ReCaptcha:SiteKey"]!)
.AddReCaptchaV3HttpClient(options => options.SecretKey = builder.Configuration["ReCaptcha:SecretKey"]!);

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ReCaptcha": {
"SiteKey": "6Le_GAgpAAAAACUDETr8C09Fb-rr1ZU0eP9_5kX_",
"SecretKey": "6Le_GAgpAAAAADWRoVeXqSg7BZL_cBWwPpt_sBdp"
}
}
2 changes: 1 addition & 1 deletion src/KITT.Web.ReCaptcha.Http/KITT.Web.ReCaptcha.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Title>KITT.Web.ReCaptcha.Http</Title>
<Version>0.1.0</Version>
<Version>0.2.0</Version>
<Authors>Alberto Mori</Authors>
<Description>Contains the service which calls Google reCaptcha verification endpoint for server-side validation</Description>
<PackageProjectUrl>https://github.com/albx/KITT.Web.ReCaptcha</PackageProjectUrl>
Expand Down
64 changes: 59 additions & 5 deletions src/KITT.Web.ReCaptcha.Http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ It can be installed using the ```dotnet add package``` command or the NuGet wiza
dotnet add package KITT.Web.ReCaptcha.Http
```

## Usage
## reCaptcha v2
### Usage

The project gives you an HttpClient service which expose a *VerifyAsync* method to verify the reCaptcha response send by the user from the client.

Add the namespace ```KITT.Web.ReCaptcha.Http.v2``` to your ```Program.cs``` and use the *AddReCaptchaV2* extension method to your ```IServiceCollection``` instance:
Add the namespace ```KITT.Web.ReCaptcha.Http.v2``` to your ```Program.cs``` and use the *AddReCaptchaV2HttpClient* extension method to your ```IServiceCollection``` instance:

```
builder.Services.AddReCaptchaV2(options =>
builder.Services.AddReCaptchaV2HttpClient(options =>
{
options.SecretKey = "<your reCaptcha server-side secret key>";
});
Expand All @@ -42,7 +43,7 @@ app.MapPost("/send", async (ReCaptchaService reCaptchaService, [FromBody] SendRe
});
```

## Methods
### Methods

The ```VerifyAsync``` method has the following input parameters:

Expand All @@ -59,4 +60,57 @@ The method returns an instance of the ```ReCaptchaResponse``` class, which have
|**Success**|*bool*: whether the verification ended successfully|
|**ChallengeTimestamp**|*DateTime*: the timestamp of the challenge load|
|**Hostname**|*string*: the hostname of the site where the reCAPTCHA was solved|
|**ErrorCodes**|*IEnumerable&lt;string&gt;*: the optional list of error codes (see [Google's official documentation](https://developers.google.com/recaptcha/docs/verify#error_code_reference))|
|**ErrorCodes**|*IEnumerable&lt;string&gt;*: the optional list of error codes (see [Google's official documentation](https://developers.google.com/recaptcha/docs/verify#error_code_reference))|

## reCaptcha v3
### Usage

The project gives you an HttpClient service which expose a *VerifyAsync* method to verify the reCaptcha response send by the user from the client.

Add the namespace ```KITT.Web.ReCaptcha.Http.v3``` to your ```Program.cs``` and use the *AddReCaptchaV3HttpClient* extension method to your ```IServiceCollection``` instance:

```
builder.Services.AddReCaptchaV3HttpClient(options =>
{
options.SecretKey = "<your reCaptcha server-side secret key>";
});
```

Then you can inject the ```ReCaptchaService``` class whenever you need and call the ```VerifyAsync``` like this:

```
app.MapPost("/send", async (ReCaptchaService reCaptchaService, [FromBody] SendRequest request) =>
{
// Here you call the reCaptcha server-side validation
var captchaResponse = await reCaptchaService.VerifyAsync(request.CaptchaResponse, request.Action);
if (!captchaResponse.Success)
{
return Results.BadRequest(captchaResponse.ErrorCodes);
}
return Results.Ok();
});
```

### Methods

The ```VerifyAsync``` method has the following input parameters:

|Property|Description|
|---|---|
|**response** (Required)|*string*: The user response token provided by the reCAPTCHA client-side integration on your site.|
|**action** (Required)|*string*: The action value used to configure the reCaptcha|
|**remoteIp** (Optional)|*string*: The user's IP address. (Default: *null*)|
|**cancellationToken** (Optional)|*CancellationToken*: a cancellation token instance (Default: *CancellationToken.None*)|

The method returns an instance of the ```ReCaptchaResponse``` class, which have the following properties:

|Property|Description|
|---|---|
|**Success**|*bool*: whether the verification ended successfully|
|**Score**|*double*: the score for the request (from 0.0 to 1.0)|
|**Action**|*string*: the action name for this request|
|**ChallengeTimestamp**|*DateTime*: the timestamp of the challenge load|
|**Hostname**|*string*: the hostname of the site where the reCAPTCHA was solved|
|**ErrorCodes**|*IEnumerable&lt;string&gt;*: the optional list of error codes (see [Google's official documentation](https://developers.google.com/recaptcha/docs/verify#error_code_reference))|

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class ServiceCollectionExtensions
/// <param name="services">The <see cref="IServiceCollection"/> instance</param>
/// <param name="configureOptions">The action used to configure the <see cref="ReCaptchaConfiguration"/> options</param>
/// <returns>The <see cref="IServiceCollection"/> instance for method chaining</returns>
public static IServiceCollection AddReCaptchaV2(
public static IServiceCollection AddReCaptchaV2HttpClient(
this IServiceCollection services,
Action<ReCaptchaConfiguration> configureOptions)
{
Expand Down
9 changes: 9 additions & 0 deletions src/KITT.Web.ReCaptcha.Http/v3/ReCaptchaResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace KITT.Web.ReCaptcha.Http.v3;

/// <summary>
/// Defines the response of the call to the reCaptcha verification endpoint
/// </summary>
public record ReCaptchaResponse
{
/// <summary>
Expand All @@ -10,9 +13,15 @@ public record ReCaptchaResponse
[JsonPropertyName("success")]
public bool Success { get; init; }

/// <summary>
/// Gets the score for the request
/// </summary>
[JsonPropertyName("score")]
public double Score { get; init; }

/// <summary>
/// Gets the action name for the request
/// </summary>
[JsonPropertyName("action")]
public string Action { get; init; } = string.Empty;

Expand Down
21 changes: 20 additions & 1 deletion src/KITT.Web.ReCaptcha.Http/v3/ReCaptchaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,39 @@

namespace KITT.Web.ReCaptcha.Http.v3;

/// <summary>
/// This service verifies the captcha response from the client calling the Google API
/// </summary>
public class ReCaptchaService
{
private readonly HttpClient _httpClient;

private readonly ReCaptchaConfiguration _configuration;

/// <summary>
/// Constructs the service instance
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> instance configured to call the Google API</param>
/// <param name="reCaptchaConfigurationOptions">The <see cref="IOptions{ReCaptchaConfiguration}"/> instance which contains the server side secret key</param>
/// <exception cref="ArgumentNullException">Thrown when <see cref="HttpClient"/> or <see cref="IOptions{ReCaptchaConfiguration}"/> instance is null</exception>
public ReCaptchaService(HttpClient httpClient, IOptions<ReCaptchaConfiguration> reCaptchaConfigurationOptions)
{
_httpClient = httpClient;
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_configuration = reCaptchaConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(reCaptchaConfigurationOptions));

ThrowIfConfigurationIsNotValid(_configuration);
}

/// <summary>
/// Verifies the response of the reCaptcha client side integration
/// </summary>
/// <param name="response">(Required) The user response token provided by the reCAPTCHA client-side integration on your site.</param>
/// <param name="action">(Required) The action value used to configure the reCaptcha</param>
/// <param name="remoteIp">(Optional) The user's IP address.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> instance</param>
/// <returns>The <see cref="ReCaptchaResponse"/> received from the call to the Google verification endpoint</returns>
/// <exception cref="ArgumentException">Thrown when response is null or white-space</exception>
/// <exception cref="InvalidOperationException">Thrown when the action returned from the server call does not match with the specified value from the client</exception>
public async Task<ReCaptchaResponse> VerifyAsync(string response, string action, string? remoteIp = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(response))
Expand Down
30 changes: 30 additions & 0 deletions src/KITT.Web.ReCaptcha.Http/v3/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using KITT.Web.ReCaptcha.Http.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace KITT.Web.ReCaptcha.Http.v3;

/// <summary>
/// Defines the extensions methods to register <see cref="ReCaptchaService"/> in the IoC container
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds <see cref="ReCaptchaService"/> service and configures all the options needed
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance</param>
/// <param name="configureOptions">The action used to configure the <see cref="ReCaptchaConfiguration"/> options</param>
/// <returns>The <see cref="IServiceCollection"/> instance for method chaining</returns>
public static IServiceCollection AddReCaptchaV3HttpClient(
this IServiceCollection services,
Action<ReCaptchaConfiguration> configureOptions)
{
services.Configure(configureOptions);

services.AddHttpClient();

services.AddHttpClient<ReCaptchaService>(
client => client.BaseAddress = new Uri("https://www.google.com/recaptcha/"));

return services;
}
}
4 changes: 2 additions & 2 deletions tests/KITT.Web.ReCaptcha.Http.Test/v3/ReCaptchaServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace KITT.Web.ReCaptcha.Http.Test.v3;

public class ReCaptchaServiceTest
{
private static readonly Uri _googleRecaptchaBaseUri = new Uri("https://www.google.com/recaptcha/");
private static readonly Uri _googleRecaptchaBaseUri = new("https://www.google.com/recaptcha/");

#region Ctor tests
[Theory]
Expand All @@ -17,7 +17,7 @@ public class ReCaptchaServiceTest
[InlineData(" ")]
public void Ctor_Should_Throw_Argument_Exception_If_Secret_Key_Is_Missing(string secretKey)
{
using HttpClient httpClient = new HttpClient();
using HttpClient httpClient = new();
IOptions<ReCaptchaConfiguration> reCaptchaConfigurationOptions = Options.Create(new ReCaptchaConfiguration { SecretKey = secretKey });

var ex = Assert.Throws<ArgumentException>(() => new ReCaptchaService(httpClient, reCaptchaConfigurationOptions));
Expand Down

0 comments on commit 51c654d

Please sign in to comment.