Skip to content

Instantly share code, notes, and snippets.

@adnanalbeda
Last active August 15, 2024 10:50
Show Gist options
  • Save adnanalbeda/69b9238a6176e4e59c283eb1930e6076 to your computer and use it in GitHub Desktop.
Save adnanalbeda/69b9238a6176e4e59c283eb1930e6076 to your computer and use it in GitHub Desktop.
Google ReCaptcha V3 - ASP API - C#

Google ReCaptcha V3 (ASP API - C#)

namespace Google.ReCaptcha.V3;
public class CaptchaRequestException : Exception
{
public CaptchaRequestException()
{
}
public CaptchaRequestException(string message)
: base(message)
{
}
public CaptchaRequestException(string message, Exception inner)
: base(message, inner)
{
}
}
using Microsoft.Extensions.DependencyInjection;
namespace Google.ReCaptcha.V3;
public static class Extensions
{
public static IServiceCollection AddGoogleRecaptchaV3(this IServiceCollection services)
{
services.AddHttpClient<IGoogleRecaptchaV3Client, GoogleRecaptchaV3Client>();
services.AddTransient<IGoogleRecaptchaV3Client, GoogleRecaptchaV3Client>();
return services;
}
}
using System.Runtime.Serialization.Json;
using System.Web;
namespace Google.ReCaptcha.V3;
public class GoogleRecaptchaV3Client : IGoogleRecaptchaV3Client
{
private readonly HttpClient _httpClient;
public RequestModel? Request { get; set; }
public ResponseModel? Response { get; set; }
public HttpRequestException? HttpReqException { get; set; }
public Exception? GeneralException { get; set; }
public GoogleRecaptchaV3Client(HttpClient httpClient)
{
_httpClient = httpClient;
}
public void InitializeRequest(RequestModel request)
{
Request = request;
}
public async Task<bool> Execute()
{
// Notes on error handling:
// Google will pass back a 200 Status Ok response if no network or server errors occur.
// If there are errors in on the "business" level, they will be coded in an array;
// CaptchaRequestException is for these types of errors.
// CaptchaRequestException and multiple catches are used to help seperate the concerns of
// a) an HttpRequest 400+ status code
// b) an error at the "business" level
// c) an unpredicted error that can only be handled generically.
// It might be worthwhile to implement a "user error message" property in this class so the
// calling procedure can decide what, if anything besides a server error, to return to the
// client and any client handling from there on.
try
{
ArgumentNullException.ThrowIfNull(Request);
//Don't to forget to invoke any loggers in the logic below.
//formulate request
string builtURL = Request.path + '?' + HttpUtility.UrlPathEncode($"secret={Request.secret}&response={Request.response}&remoteip={Request.remoteip}");
StringContent content = new StringContent(builtURL);
Console.WriteLine($"Sent Request {builtURL}");
//send request, await.
HttpResponseMessage response = await _httpClient.PostAsync(builtURL, null);
response.EnsureSuccessStatusCode();
//read response
byte[] res = await response.Content.ReadAsByteArrayAsync();
string logres = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Retrieved Response: {logres}");
//Serialize into GReponse type
using (MemoryStream ms = new MemoryStream(res))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(ResponseModel));
Response = (ResponseModel)serializer.ReadObject(ms)!;
}
//check if business success
if (!Response.success)
{
throw new CaptchaRequestException();
}
//return bool.
return true; //response.IsSuccessStatusCode; <- don't need this. EnsureSuccessStatusCode is now in play.
}
catch (HttpRequestException hre)
{
//handle http error code.
HttpReqException = hre;
//invoke logger accordingly
//only returning bool. It is ultimately up to the calling procedure
//to decide what data it wants from the Service.
return false;
}
catch (CaptchaRequestException ex)
{
//Business-level error... values are accessible in error-codes array.
//this catch block mainly serves for logging purposes.
/* Here are the possible "business" level codes:
missing-input-secret The secret parameter is missing.
invalid-input-secret The secret parameter is invalid or malformed.
missing-input-response The response parameter is missing.
invalid-input-response The response parameter is invalid or malformed.
bad-request The request is invalid or malformed.
timeout-or-duplicate The response is no longer valid: either is too old or has been used previously.
*/
//invoke logger accordingly
//only returning bool. It is ultimately up to the calling procedure
//to decide what data it wants from the Service.
return false;
}
catch (Exception ex)
{
// Generic unpredictable error
GeneralException = ex;
// invoke logger accordingly
//only returning bool. It is ultimately up to the calling procedure
//to decide what data it wants from the Service.
return false;
}
}
}
namespace Google.ReCaptcha.V3;
/// <summary>
/// Register with: `AddHttpClient`
/// </summary>
public interface IGoogleRecaptchaV3Client
{
RequestModel? Request { get; set; }
ResponseModel? Response { get; set; }
void InitializeRequest(RequestModel request);
Task<bool> Execute();
}
// const site_key = "";
// var script = document.createElement("script");
// script.type = "text/javascript";
// script.src =
// "https://www.google.com/recaptcha/api.js?render=" + site_key;
// document.body.appendChild(script);
// const getReCaptchaToken = (action = "submit") => grecaptcha.ready(() => grecaptcha.execute(site_key, { action } ));
const loadReCaptcha = (site_key) => {
if (!site_key || typeof site_key !== "string" || site_key.length < 10) throw new Error("Invalid site_key.");
if (grecaptcha) return;
const captcha = {
key: site_key,
loaded:false,
getToken: (action = "submit") => grecaptcha.ready(() => grecaptcha.execute(site_key, { action } ))
}
var script = document.createElement("script");
script.type = "text/javascript";
script.src = `https://www.google.com/recaptcha/api.js?render=${site_key}`;
script.addEventListener("load", () => {captcha.loaded = true;})
document.body.appendChild(script);
return captcha;
}
namespace Google.ReCaptcha.V3;
public class RequestModel
{
// CONFIG:CAPTCHA_URL :: `https://www.google.com/recaptcha/api/siteverify`
/// <summary>
/// Fill From: <br/>
/// CONFIG:CAPTCHA_URL :: `https://www.google.com/recaptcha/api/siteverify`
/// </summary>
public required string path { get; set; }
// CONFIG:CAPTCHA_SECRET
/// <summary>
/// Fill From: <br/>
/// CONFIG:CAPTCHA_SECRET
/// </summary>
public required string secret { get; set; }
// CLIENT_POST_DATA:RecaptchaToken
/// <summary>
/// Fill From: <br/>
/// CLIENT_POST_DATA:RecaptchaToken
/// </summary>
public required string response { get; set; }
// HttpContext.Connection.RemoteIpAddress.ToString()
/// <summary>
/// Fill From: <br/>
/// HttpContext.Connection.RemoteIpAddress.ToString()
/// </summary>
public required string remoteip { get; set; }
}
using System.Runtime.Serialization;
namespace Google.ReCaptcha.V3;
// Google's response property naming is
// embarrassingly inconsistent, that's why we have to
// use DataContract and DataMember attributes,
// so we can bind the class from properties that have
// naming where a C# variable by that name would be
// against the language specifications... (i.e., '-').
[DataContract]
public class ResponseModel
{
[DataMember]
public bool success { get; set; }
[DataMember]
public double score { get; set; }
[DataMember]
public string challenge_ts { get; set; } // or datetime
[DataMember]
public string hostname { get; set; }
[DataMember]
public string action { get; set; }
//Could create a child object for
//error-codes
[DataMember(Name = "error-codes")]
public string[] error_codes { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment