DEV Community

loading...
Cover image for Google reCaptcha v3 server-side validation using ASP.NET Core 5.0

Google reCaptcha v3 server-side validation using ASP.NET Core 5.0

spencer741 profile image Spencer Arnold ・14 min read

There are quite a few snippets on Github and NuGet packages out there that try to implement server-side Google reCaptcha validation. Some of the NuGet packages will even cover the entire reCaptcha process—all the way from embedding the Google-provided reCaptcha script in a razor page to exposing a middleware to call in your request pipeline.

All of that is good and well, but there are a few problems. One problem is that some of these implementations aren't maintained (or don't even support reCatpcha V3), but worst of all some of them won't scale, thus falling short of very basic requirements for a solid production build.

But why won't they scale?

Usually it's the misuse of HttpClient, or rather the lack of using HTTPClientFactory. Some code out there will instantiate an HttpClient on every incoming reCaptcha validation request! This can prove costly and fatal for a couple of reasons:

1) Each HttpClient gets its own connection pool.
2) Socket Exhaustion when a bunch are spawned.

Easter Egg 🥚
I got exposed to ASP.NET Core around the time 2.1 released, in which I remember reading an amazing article by Microsoft MVP Steve Gordan on the new HttpClientFactory coming to 2.1 and what problems it was solving. It has a lot of good detail pertaining to this feature.

Aside from that; however, I wanted to add reCaptcha functionality on the controller level, not just exposing it as a middleware in the pipeline. This would allow me to control what endpoints I explicitly need reCaptcha validation on. In addition, I am running a React SPA, so I didn't necessarily need Razor functionality or any extra bloat that comes along with some of these packages. Just a simple and sweet reCaptcha server-side validation mechanism.

This can all be done with Dependency Injection (DI), an HttpClientFactory, built in JSON serialization, and of course some asynchronous flair.

Let's get to it!

1. Pre-req's

In addition to the basics, here are some high-level concepts/technologies to be familiar with (non-inclusive).

2. Process Overview

Here's the "explain it like I'm 5" overview of the back-end process to hard validate a Google Captcha exchange:

  • Your Server: Hey Google, there's a dude that tells me that he's not a robot. He says that you already verified that he's a human, and he told me to give you this token as a proof of that.
  • Google: Hmm... let me check this token... yes I remember this dude I gave him this token... yeah he's made of flesh and bone let him through.
  • Your Server: Hey Google, there's another dude that tells me that he's a human. He also gave me a token.
  • Google: Hmm... I never issued this token... I'm pretty sure this guy is trying to fool you. Tell him to get off your site.

-- courtesy of TheRueger on SO

Simple enough!

3. Some housekeeping

Now, before we move on, make sure you read over some of Google's documentation. Regardless of where you are in development (whether you have embedded the reCaptcha script in your front-end yet or are just getting started), you will need to make sure you're following the Google reCaptchaV3 tutorial, which contains info on front-end setup and registering your reCaptcha v3 keys.

For the purposes of this tutorial, you just need the Secret Key that is provided in the Admin Console under your site.
Alt Text

Easter Egg 🥚
It is good practice to have two key pairs (one for development and one for production. This way, you can add localhost to the domain inclusion list for dev).

4. First stop: appsettings.json

In this step, we are going to add our reCaptcha Secret Key to appsettings.json. We will be accessing it later on to pass to our custom Google reCaptcha service. This key (in addition to the token passed from our client) will help Google determine if the validation request and the Captcha is legitimate.

To stay semantically sound here, we are going to add a properties called "Secret" and "ApiUrl".

"GoogleRecaptchaV3": {
    "Secret": "yoursecretapikey",
    "ApiUrl": "https://www.google.com/recaptcha/api/siteverify"
}
Enter fullscreen mode Exit fullscreen mode

After you've added the snippet, your appsettings.json might look something like below. You have logging, allowed hosts, etc, with the "Secret" and "ApiUrl" path properties conveniently added under "GoogleRecaptchaV3".

Of course if you have a different structure in mind or have to integrate with an existing setup that's already built out, do it how you see fit. It will be fairly easy to access properties at any depth in the code. This example is just for the demo.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "GoogleRecaptchaV3": {
    "Secret": "yoursecretapikey",
    "ApiUrl": "https://www.google.com/recaptcha/api/siteverify"
  }
}
Enter fullscreen mode Exit fullscreen mode

You'll probably also have your database connection string and your site key for Google reCaptcha in your appsettings.json, but for simplicity I am only including the property for my secret key. If you update your key and changes are not persisting, appsettings.json, make sure to completely restart your server.

Disclaimer ⚠️
Don't forget to add appsettings.json to your .gitignore. You don't want your secrets to get on the web! This is the whole purpose of environment variables and appsettings.json.
Easter Egg 🥚
You can also look into using appsettings.Development.json if you are using production and development keys separately (which is a good idea).

5. Building the Request and Response Models

To make our life easy, we will be creating a class that represents the request sent to Google and another class for the response. These classes make it straight forward to initialize a request and serialize the response content accordingly.

Remember, these are 1:1 request/response representations based on Google's API spec.

For the demo, I am putting the models in a file called GHttpModels.cs. We will use these classes when we build the GoogleRecaptchaV3 service class.

using System;
using System.Runtime.Serialization;

namespace AspNetCoreRecaptchaV3ValidationDemo.Tooling
{

    public class GRequestModel
    {
        public string path { get; set; }
        public string secret { get; set; }
        public string response { get; set; }
        public string remoteip { get; set; }

        public GRequestModel(string res, string remip)
        {
            response = res;
            remoteip = remip;
            secret = Startup.Configuration["GoogleRecaptchaV3:Secret"];
            path = Startup.Configuration["GoogleRecaptchaV3:ApiUrl"];
            if(String.IsNullOrWhiteSpace(secret) || String.IsNullOrWhiteSpace(path))
            {
                //Invoke logger
                throw new Exception("Invalid 'Secret' or 'Path' properties in appsettings.json. Parent: GoogleRecaptchaV3.");
            }
        }
    }

    //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 GResponseModel
    {
        [DataMember]
        public bool success { get; set; }
        [DataMember]
        public string challenge_ts { get; set; }
        [DataMember]
        public string hostname { get; set; }

        //Could create a child object for 
        //error-codes
        [DataMember(Name = "error-codes")]
        public string[] error_codes { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you are typing while following along, you will probably notice your linter complaining. Not to fear! This time, it's expected.

The Startup.Configuration[...] lines are not valid because the Configuration property doesn't exist in Startup.cs yet. Don't worry, we will get to it and some other important things in Startup.cs after we finish developing the GoogleRecaptchaV3 service class.

Onward!

6. Creating the GoogleRecaptchaV3 Service

In case you missed out on it:

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. In the typical "using" relationship the receiving object is called a client and the passed (that is, "injected") object is called a service. source

If you haven't guessed it by now, we are building a DI service that we will add to the ASP.NET Core DI container!

In addition to our (not yet written) controller requiring a GoogleRecaptchaV3Service instance from the DI container, the GoogleRecaptchaV3Service itself also has a dependency: HttpClient.

Below, you can see we define an interface. Our service class inherits from that interface. This interface could be useful for further development of other features, although one could argue it is bloated OO syntactic sugar for now. (Passing around interfaces can also be useful to implement some very powerful testing functionality).

[breakdown below]

using System;
using System.Threading.Tasks;
using System.Text.Json;
using System.Web;
using System.Net.Http;
using System.IO;
using System.Text;
using System.Runtime.Serialization.Json;

namespace AspNetCoreRecaptchaV3ValidationDemo.Tooling
{
    public class CaptchaRequestException : Exception
    {
        public CaptchaRequestException()
        {
        }
        public CaptchaRequestException(string message)
            : base(message)
        {
        }
        public CaptchaRequestException(string message, Exception inner)
            : base(message, inner)
        {
        }
    }

    public interface IGoogleRecaptchaV3Service
    {
        HttpClient _httpClient { get; set; }
        GRequestModel Request { get; set; }
        GResponseModel Response { get; set; }
        void InitializeRequest(GRequestModel request);
        Task<bool> Execute();
    }

    public class GoogleRecaptchaV3Service : IGoogleRecaptchaV3Service
    {
        public HttpClient _httpClient { get; set; }

        public GRequestModel Request { get; set; }

        public GResponseModel Response { get; set; }

        public HttpRequestException HttpReqException { get; set; }

        public Exception GeneralException { get; set; }

        public GoogleRecaptchaV3Service(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public void InitializeRequest(GRequestModel 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
            {

                //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(GResponseModel));
                    Response = (GResponseModel)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;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's break the service class down...

Our service is comprised of two methods:Initialize and Execute.

Initialize takes a GRequestModel object as an argument. Normally you would see that parameter in the constructor, but since we want granular control over this at the controller scope, I separated it into another method. The reason for this is that the reCaptcha token from the client will likely be appended to request data. Let's say you have a form that is submitted via an aJax request of some sort. When the submit button is clicked, you can programmatically trigger the front-end reCaptcha script to do it's magic, communicate with Google, and give you back a token if the user passes the V3 robot test. Once you get that token in a callback, you will likely append that token to the JSON object that you previously collected your field values into.

Server-side, when the request hits the controller, it is binded to the model specified. From there, you will need to extract the pertinent reCaptcha information and pass it to the already instantiated (by DI) GoogleReCaptchaV3Service. Don't worry if this doesn't make sense yet, when you see the controller logic at the end it will all come together.

The execute function is an Task that manages all of our asynchronous code. This task formulates the request, sends it to Google, awaits, and de-serializes the response into a strongly typed, ready to grab GResponseModel object.

I am putting all this code in file called GoogleRecaptchaV3Service.cs, under the same generic "Tooling" namespace that our models are children to. Note the error handling and custom exception handler that I also snuck in there... more on that below.

That's basically the entire service!

You will notice comments about the extensive error handling that is taking place. There are three possible types of errors that cover our error surface within the functionality of the service class. Note that I created a custom exception handler for separation of concerns, which is nothing but the stock exception handler under a different alias. It's purpose is to separate an unpredictable generic error from the predictable error list that Google passes back to us if there is a "business-level" error (with a status code 200).

Note that more functionality could be added to the service. For example, adding a logger dependency! This would be especially useful if you wanted to perform unstructured or structured logging to an external data store or service like Azure Application Insights using Serilog.

7. Exposing the configuration in Startup.cs

The next step is to make sure we can access the configuration builder so we can get our reCaptcha secret from appsettings.json. This seems to be a highly opinionated topic out on the web, with people coming up with some crazy, bloated, bells-and-whistles-dependency-injection-jujitsu, but there is a VERY simple approach that will work just fine for our circumstances.

Since Asp.Net Core 2.0, the default initialization of WebHostBuilder (which can be found in program.cs) automatically adds an IConfiguration instance to the DI container (the proof is in the pudding).

This means that all we have to do is add IConfiguration as a dependency of our Startup class and set it as a property. Since most of our files are in the same namespace, we can just access it directly from pretty much anywhere (like we do in our GRequestModel: Startup.Configuration["GoogleRecaptchaV3:Secret"];)

using Microsoft.Extensions.Configuration;
using AspNetCoreRecaptchaV3ValidationDemo.Tooling;

public class Startup
{
   internal static IConfiguration Configuration { get; private set; }
   public Startup(IConfiguration configuration)
   {
       Configuration = configuration;
   }
}

Enter fullscreen mode Exit fullscreen mode

8. Adding pertinent services to the DI container

Here we use ConfigureServices to add services to the DI container. If you don't have the ConfigureServices method in your startup, you can just paste it in.

If you're new to all this, make sure you understand the difference between ConfigureServices and Configure.

  • ConfigureServices : Where you configure the DI container.
  • Configure : Where you configure the Http request pipeline.
public void ConfigureServices(IServiceCollection services)
{          
    services.AddHttpClient<IGoogleRecaptchaV3Service,GoogleRecaptchaV3Service>();
    services.AddTransient<IGoogleRecaptchaV3Service,GoogleRecaptchaV3Service>();
    services.AddControllers();
}
Enter fullscreen mode Exit fullscreen mode

I wasn't going to include this part, but don't forget to also configure UseEndpoints in your Http Request pipeline so your requests are routed to your controllers.

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});
Enter fullscreen mode Exit fullscreen mode

9. Setting up a Controller to use our GoogleRecaptchaV3Service

Below, you can see a standard controller implementation that accepts some sign up form data from the body of the request (in the form of the SignUpModel, which contains the Recaptcha token).

As you can see, we added a constructor to our controller, specifying IGoogleRecaptchaV3Service as a dependency. We then set it as a property, to then later perform initialization and execution when a request hits the controller.

    public class SignUpModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string RecaptchaToken { get; set; }
    }

    [ApiController]
    [Route("public/signup")]
    public class SignUp : ControllerBase
    {
        IGoogleRecaptchaV3Service _gService { get; set; }
        public SignUp(IGoogleRecaptchaV3Service gService)
        {
            _gService = gService;
        }

        [HttpPost]
        public async Task<IActionResult> Post([FromQuery] SignUpModel SignUpData)
        {

            GRequestModel rm = new GRequestModel(SignUpData.RecaptchaToken,
                                                 HttpContext.Connection.RemoteIpAddress.ToString());

            _gService.InitializeRequest(rm);

            if(!await _gService.Execute())
            {
                //return error codes string.
                return Ok(_gService.Response.error_codes);
            }

            //call Business layer

            //return result
            return Ok("Server-side Google reCaptcha validation successfull!");

        }
    }
Enter fullscreen mode Exit fullscreen mode
Easter Egg 🥚
When using Entity Framework in this context, you can always use inheritance to create new types, that is if you have data you want to accept through your Web api which is not represented in your data model (i.e.,if it doesn't need to be stored persistently).

10. Testing it out

Almost Done!

I created a very basic demo where you can see a more complete picture of the snippets in this article. If you want to run the code, you can to put your secret key in appsettings.json (or you could see what happens when there's an invalid secret key in appsettings.json 😉).

Alt Text

The example will allow you to insert your site key in-browser at the root path / and run an end-to-end mock of the entire process (I dynamically inject the Google reCaptcha script after you input your site key so you don't have to dig around in the html. Alternatively, I could have used Razor Pages + appsettings.json). On that note I have not .gitignore'd appsettings.json so feel free to view it in Github. Just be keen on where this code ends up if you include any secret keys.

GitHub logo spencer741 / GoogleRecaptchaV3Service

Google reCaptcha v3 server-side validation using ASP.NET Core 5.0

Thanks for reading! If there are any typos or grammatical issues, just comment below so I can amend them.


Give me a follow on Dev.to, Github, or LinkedIn if you liked the article and want to see more! If you would like, you can also buy me a coffee. Any support is greatly appreciated!

> Connect with me
Enter fullscreen mode Exit fullscreen mode

My LinkedIn profile GitHub

> Support me
Enter fullscreen mode Exit fullscreen mode

Buy Me A Coffee

> Support everybody
Enter fullscreen mode Exit fullscreen mode

The ability to transfer money has been a long-standing omission from the web platform. As a result, the web suffers from a flood of advertising and corrupt business models. Web Monetization provides an open, native, efficient, and automatic way to compensate creators, pay for API calls, and support crucial web infrastructure. Learn More about how you can help change the web.


Q: Why not use dependency injection in attributes?
A: Dependency Injection in Attributes: don’t do it!

Q: Why not just build a "Perform Server-Side Recaptcha validation" endpoint?
A: This ultimately makes things more complex. By this philosophy, the front-end reCaptcha script communicates with Google, gives you back a token, you take that token and call this theoretical Server-Side Recaptcha validation api. That api either has to persist knowledge that it did indeed perform validation (by server-side storage, or by returning a token of its own—which adds payload and complexity). How else would the secondary call submitting, say the sign up information, prove to the server that it had indeed been server-side validated before? If you wanted to take this approach, might as well forget about REST. This is a good lesson on stateless protocols.

Q: Why didn't you include the GRequest model in the DI container?
A: First, I will point you to this SO post. I will also add that adding GRequest as a dependency to GoogleRecaptchaV3Service would kill my intention of granualirty at the controller level. If you are thinking about adding GRequest as a dependency at the controller's constructor, that would mean extracting data upstream... you can see where that's going...this should really be done in the controller in this circumstance. As for reading body data multiple times, it should probably only be done in special circumstances. I could go on, but I will stop here :)


Discussion (0)

pic
Editor guide