Refactoring
This commit is contained in:
131
AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs
Normal file
131
AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom filter attribute to use Google's reCaptcha (v3).
|
||||
/// Usage: [ServiceFilter(typeof(GoogleReCaptchaAttribute))]
|
||||
/// </summary>
|
||||
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 readonly string privateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GoogleReCaptchaAttribute"/> class.
|
||||
/// </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>
|
||||
/// <param name="configuration">The application configuration.</param>
|
||||
public GoogleReCaptchaAttribute(IConfiguration configuration)
|
||||
{
|
||||
privateKey = configuration.GetValue<string>("Google:ReCaptcha: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)
|
||||
{
|
||||
await DoValidation(context);
|
||||
await base.OnActionExecutionAsync(context, next);
|
||||
}
|
||||
|
||||
private async Task DoValidation(ActionExecutingContext context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(privateKey))
|
||||
return;
|
||||
|
||||
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<string, string>
|
||||
{
|
||||
{ "secret", privateKey },
|
||||
{ "response", token }
|
||||
};
|
||||
var response = await httpClient.PostAsync(VerificationUrl, new FormUrlEncodedContent(param));
|
||||
string json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user