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).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 { { "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(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; } } } }