1
0
Files
common/AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs

129 lines
3.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc.Filters
{
/// <summary>
/// Custom filter attribute to use Google's reCaptcha (v3).
/// <br/>
/// Usage: [ServiceFilter(typeof(GoogleReCaptchaAttribute))]
/// </summary>
/// <remarks>
/// appsettings.json:
/// <br/>
/// <code>
/// {<br/>
/// [...]<br/>
/// "Google": {<br/>
/// "ReCaptcha": {<br/>
/// "PrivateKey": "__private reCaptcha key__",<br/>
/// "PublicKey": "__public reCaptcha key__"<br/>
/// }<br/>
/// }<br/>
/// }
/// </code>
/// <br/>
/// The score from google can be found on HttpContext.Items[GoogleReCaptchaAttribute.ScoreKey].
/// </remarks>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class GoogleReCaptchaAttribute : ActionFilterAttribute
{
/// <summary>
/// The error key used in <see cref="ActionContext.ModelState"/>.
/// </summary>
public const string ErrorKey = "GoogleReCaptcha";
/// <summary>
/// The key used in forms submitted to the backend.
/// </summary>
public const string ResponseTokenKey = "g-recaptcha-response";
/// <summary>
/// The key used in <see cref="Http.HttpContext.Items"/> to transport the score (0 - bot, 1 - human).
/// </summary>
public const string ScoreKey = "GoogleReCaptchaScore";
private const string VerificationUrl = "https://www.google.com/recaptcha/api/siteverify";
private string privateKey;
/// <summary>
/// Executes the validattion in background.
/// </summary>
/// <param name="context">The action context.</param>
/// <param name="next">The following action delegate.</param>
/// <returns>An awaitable task.</returns>
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
privateKey = configuration?.GetValue<string>("Google:ReCaptcha:PrivateKey");
if (string.IsNullOrWhiteSpace(privateKey))
return;
await DoValidation(context).ConfigureAwait(false);
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
}
private async Task DoValidation(ActionExecutingContext context)
{
if (!context.HttpContext.Request.HasFormContentType)
return;
var token = context.HttpContext.Request.Form[ResponseTokenKey];
if (string.IsNullOrWhiteSpace(token))
{
context.ModelState.TryAddModelError(ErrorKey, "No token to validate Google reCaptcha");
return;
}
await Validate(context, token).ConfigureAwait(false);
}
private async Task Validate(ActionExecutingContext context, string token)
{
using var httpClient = new HttpClient();
var param = new Dictionary<string, string>
{
{ "secret", privateKey },
{ "response", token }
};
var response = await httpClient.PostAsync(VerificationUrl, new FormUrlEncodedContent(param)).ConfigureAwait(false);
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var result = JsonConvert.DeserializeObject<Response>(json);
if (result?.Success != true)
{
context.ModelState.TryAddModelError(ErrorKey, "Google reCaptcha verification failed");
context.HttpContext.Items[ScoreKey] = null;
}
else
{
context.HttpContext.Items[ScoreKey] = result.Score;
}
}
private class Response
{
public bool Success { get; set; }
public decimal Score { get; set; }
public string Action { get; set; }
[JsonProperty("challenge_ts")]
public DateTime Timestamp { get; set; }
public string Hostname { get; set; }
[JsonProperty("error-codes")]
public List<string> ErrorCodes { get; set; }
}
}
}