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
{
///
/// Custom filter attribute to use Google's reCaptcha (v3).
///
/// Usage: [ServiceFilter(typeof(GoogleReCaptchaAttribute))]
///
///
/// appsettings.json:
///
///
/// {
/// [...]
/// "Google": {
/// "ReCaptcha": {
/// "PrivateKey": "__private reCaptcha key__",
/// "PublicKey": "__public reCaptcha key__"
/// }
/// }
/// }
///
///
/// The score from google can be found on HttpContext.Items[GoogleReCaptchaAttribute.ScoreKey].
///
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class GoogleReCaptchaAttribute : ActionFilterAttribute
{
///
/// The error key used in .
///
public const string ErrorKey = "GoogleReCaptcha";
///
/// The key used in forms submitted to the backend.
///
public const string ResponseTokenKey = "g-recaptcha-response";
///
/// The key used in to transport the score (0 - bot, 1 - human).
///
public const string ScoreKey = "GoogleReCaptchaScore";
private const string VerificationUrl = "https://www.google.com/recaptcha/api/siteverify";
private string privateKey;
///
/// Executes the validattion in background.
///
/// The action context.
/// The following action delegate.
/// An awaitable task.
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var configuration = context.HttpContext.RequestServices.GetService();
privateKey = configuration?.GetValue("Google:ReCaptcha:PrivateKey");
if (string.IsNullOrWhiteSpace(privateKey))
return;
await DoValidation(context);
await base.OnActionExecutionAsync(context, next);
}
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);
}
private async Task Validate(ActionExecutingContext context, string token)
{
using var httpClient = new HttpClient();
var param = new Dictionary
{
{ "secret", privateKey },
{ "response", token }
};
var response = await httpClient.PostAsync(VerificationUrl, new FormUrlEncodedContent(param));
string json = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject(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 ErrorCodes { get; set; }
}
}
}