129 lines
3.9 KiB
C#
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; }
|
|
}
|
|
}
|
|
}
|