DEV Community

Cover image for Part II: Google reCaptcha v3 server-side validation using ASP.NET Core 5.0
Spencer Arnold
Spencer Arnold

Posted on • Edited on

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

After writing Google reCaptcha v3 server-side validation using ASP.NET Core 5.0, I was able to happily use the code in production apps. However, as it usually happens in the software world, needs arise to iterate.

Here's a short part II.

The main problem the previous code didn't solve? Validating captchas server-side with multiple "Sites" (as Google calls them) or "site-key/secret-key pairs".

ReCaptchas can become extremely effective by taking advantage of separation of concerns from an analytics perspective. From the reCaptcha dashboard, you can view reCaptcha analytics for each "Site."

In my specific use case, I had a "Gateway Service" that handled serving up the login, registration, password reset, live redirecting functionality, etc. I also had a landing page service that handled all landing page functionality (including any initial contact forms or newsletter signups). Since most of these services are open to the public, I had the need to put some level of captcha protection a lot of this.

Now, it's possible to just stick with the old code, which only supports one "site-key/secret-key pair" for all reCaptcha validation. And... it works....

Sample ReCaptcha Dashboard

The downside, though, is that you don't have much insight from an analytics perspective. You could have millions of requests cumulatively between the login, signup, forgot password, newsletter, and contact forms--all being processed through one "site-key/secret-key pair".

But so what? Having one site-key/secret-key pair seems to work and be a fairly simple implementation... But what happens when you have millions of requests between forms and you get an influx of Google-marked "suspicious" requests? In this case, it could prove quite useful to know which forms are getting hit with the majority of the flagged requests. Separating things out could give you insight to mitigate with precision. The Google ReCaptcha dashboards can be insightful, so why not distribute the load with multiple site-key/secret-key pairs to gain precise insight?

A Factory Solution:

So, expanding on the code already written, I have added a factory to allow for multiple site-key/secret-key pair validation. The factory allows us to specify a specific "Site" post-DI injection. The factory has some custom methods that also allow for execution of the reCaptcha validation in one step with the under-the-hood class instantiation.

Note: I have omitted code from the previous article that defines the core GoogleRecaptchaV3Service, for the sake of brevity.

The Factory:

public class GoogleRecaptchaV3ServiceFactoryOption<T> where T : IComparable
{
    public T Id { get; set; }
    public string ApiUrl { get; set; }
    public string SecretKey { get; set; }
}

public class GoogleRecaptchaV3ServiceFactory<T> where T : IComparable
{
    List<GoogleRecaptchaV3ServiceFactoryOption<T>> GoogleRecaptchaV3ServiceFactoryOptions { get; set; }
    HttpClient HttpClient { get; set; }

    public GoogleRecaptchaV3ServiceFactory(HttpClient httpClient,IOptions<List<GoogleRecaptchaV3ServiceFactoryOption<T>>> options)
    {
        GoogleRecaptchaV3ServiceFactoryOptions = options.Value;
        HttpClient = httpClient;
    } 

    public GoogleRecaptchaV3Service Create(T Id, string response, string remoteIp)
    {
        GoogleRecaptchaV3ServiceFactoryOption<T> selectedOption = 
        GoogleRecaptchaV3ServiceFactoryOptions.FirstOrDefault(x => x.Id.Equals(Id));
        return new GoogleRecaptchaV3Service(HttpClient, selectedOption.ApiUrl, selectedOption.SecretKey, response, remoteIp);
    }

    public async Task<GoogleRecaptchaV3Service> CreateAndExecute(T Id, string response, string remoteIp)
    {
        GoogleRecaptchaV3ServiceFactoryOption<T> selectedOption = GoogleRecaptchaV3ServiceFactoryOptions.FirstOrDefault(x => x.Id.Equals(Id));
        GoogleRecaptchaV3Service googleRecaptchaV3Service = new GoogleRecaptchaV3Service(HttpClient, selectedOption.ApiUrl, selectedOption.SecretKey, response, remoteIp);
        await googleRecaptchaV3Service.Execute();
        return googleRecaptchaV3Service;
    }
}

// ENUM to represent all Recaptcha Configurations.
public enum ReCaptchaConfigs { Gateway, Landing, Portal, SignUp, Login, MultiFactor, ForgotPasswordRequest, ForgotPasswordReset };
Enter fullscreen mode Exit fullscreen mode

Now, You can register the factory and all the options in the DI Container.

ConfigureServices in startup.cs might look something like this:

services.AddHttpClient<GoogleRecaptchaV3ServiceFactory<ReCaptchaConfigs>>();
services.AddTransient<GoogleRecaptchaV3ServiceFactory<ReCaptchaConfigs>>();

services.Configure<List<GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>>>(options =>
{
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.Landing,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:landing:secret"]
    });
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.Portal,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:portal:secret"]
    });
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.SignUp,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:signup:secret"]
    });
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.Login,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:login:secret"]
    });
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.MultiFactor,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:multifactor:secret"]
    });
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.ForgotPasswordRequest,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:forgotpasswordrequest:secret"]
    });
    options.Add(new GoogleRecaptchaV3ServiceFactoryOption<ReCaptchaConfigs>
    {
        Id = ReCaptchaConfigs.ForgotPasswordReset,
        ApiUrl = Configuration["GoogleRecaptchaV3:ApiUrl"],
        SecretKey = Configuration["GoogleRecaptchaV3:forgotpasswordreset:secret"]
    });
});
Enter fullscreen mode Exit fullscreen mode

And a section of your app.settings might look like:

"GoogleRecaptchaV3": {
    "ApiUrl": "https://www.google.com/recaptcha/api/siteverify",
    "landing": {
      "site": "<key>",
      "secret": "<key>"
    },
    "portal": {
      "site": "<key>",
      "secret": "<key>"
    },
    "signup": {
      "site": "<key>",
      "secret": "<key>"
    },
    "login": {
      "site": "<key>",
      "secret": "<key>"
    },
    "multifactor": {
      "site": "<key>",
      "secret": "<key>"
    },
    "forgotpasswordrequest": {
      "site": "<key>",
      "secret": "<key>"
    },
    "forgotpasswordreset": {
      "site": "<key>",
      "secret": "<key>"
    }
  },
Enter fullscreen mode Exit fullscreen mode

And finally, an example controller. I omitted the DI part for brevity.

[HttpPost]
public async Task<IActionResult> Post([FromBody] SignUpDataCheck MobileCheck)
{
    // Result and ResultStatus are just a standard Result pattern implementation.
    Result gstat = (await _GoogleRecaptchaV3ServiceFactory.CreateAndExecute(
                    ReCaptchaConfigs.SignUp, MobileCheck.RecaptchaToken,
                    HttpContext.Connection.RemoteIpAddress.ToString())).result;

    switch (gstat.status)
    {
        case ResultStatus.Rejected:
            return StatusCode(400, gstat.statusMessage);

        case ResultStatus.Exception:
            return StatusCode(500, "Error reported. Try again later.");
    }
}

Enter fullscreen mode Exit fullscreen mode

By passing the ReCaptchaConfigs. into the factory, we can selectively use the secret-keys associated with each individual key-pair to validate the exact "Site" we want in our endpoints.

So that's it! No fancy demo this time, but you can always view the previous code on Github.

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.

Top comments (0)