Adding CAPTCHA on form posts with ASP.NET Core 🚦

We all know and dislike them. Those pain-in-the-... CAPTCHA challenges you need to complete to complete form posts online. I cannot count the number of fire hoses, trains, trucks, and light signals I have identified over time. But, while annoying, CAPTCHASs also serve a purpose. In this post, I'll introduce you to CAPTCHAs and show you how to add one in ASP.NET Core using the reCAPTCHA service.

So, what is a CAPTCHA and why do we need it? CAPTCHA is an acronym for Completely Automated Public Turing test to tell Computers and Humans Apart. Luckily most people will know what you are talking about when using the term CAPTCHA πŸ˜‚ A CAPTCHA is exactly what the full name says: A way to test if someone is a computer or a human.

There are many situations online where you want to be able to tell humans and computers apart. Form posts being a good example to avoid automated crawlers making form posts. The example I'll be using throughout this post is a form post for resetting a user's password. This is a good place to apply a CAPTCHA since you want to avoid a bot from triggering a lot of reset password requests based on leaked email lists. I'll be using an ASP.NET Core website and the reCAPTCHA service as an example. There are a couple of packages available to help you with adding captchas in ASP.NET Core, but for this post, I'll show you the native/direct way to do it. I might blog about one of the alternatives later too.

Let's create a new ASP.NET Core website through Visual Studio or your favorite CLI. I'll use Visual Studio and enable Individual User Accounts while creating the site:

To make changes to the reset password page, we need to add one of the scaffolded files not available on the file system as default:

Select Identity:

And finally, select Account\ForgotPassword Β and the Entity Framework context generated for your project:

You now have a forgot password razor page located in Areas\Identity\Pages\Account\ForgotPassword.cshtml.

To make the user solve a captcha when posting the email to reset the password for, we need the following:

  1. Obtain keys from reCAPTCHA
  2. Include reCAPTCHA on the form
  3. Verify CAPTCHA token on the backend

Obtain keys from reCAPTCHA

Signing up for reCAPTCHA can be done here: https://www.google.com/recaptcha/about/. Once signed up, you will be guided through creating a new site. For the code in this blog post to work, you will need to pick reCAPTCHA v2 during the signup process.

In the following steps, we will need both a reCAPTCHA Site Key and Secret Key. Bot keys are found on the settings screen on the new site:

Copy both keys or keep the tab open for the following steps.

Include reCAPTCHA on the form

reCAPTCHA is added by including the JavaScript from Google somewhere. In this example we only want a CAPTCHA on the reset password page, why it can be added to the Scripts section in the ForgotPassword.cshtml file:

@section Scripts {
    <script src="https://www.google.com/recaptcha/api.js" async defer></script>
    <partial name="_ValidationScriptsPartial" />    
</script>

Next, you will need to tell reCAPTCHA where to put the CAPTCHA control. Paste the following markup inside the form element:

<div class="form-group">
    <div class="g-recaptcha" data-sitekey="SITE_KEY"></div>
</div>

For the data-sitekey attribute, I'm using the reCAPTCHA site key that we copied in the previous step.

When launching the site and navigating to the forgot password page, we now see a CAPTCHA:

So far so good. In the next section, we will validate that people solved the CAPTCHA.

Verify CAPTCHA token on the backend

When solving the captcha in the browser, reCAPTCHA automatically includes a token in form POSTs including the CAPTCHA control. This token can be validated by the reCAPTCHA API. To do so, we'll need a HttpClient which I wrap in a class to make dependency injection easy. Add a new C# class:

public class ReCaptcha
{
    private readonly HttpClient captchaClient;

    public ReCaptcha(HttpClient captchaClient)
    {
        this.captchaClient = captchaClient;
    }

    public async Task<bool> IsValid(string captcha)
    {
        try
        {
            var postTask = await captchaClient
                .PostAsync($"?secret=SECRET_KEY&response={captcha}", new StringContent(""));
            var result = await postTask.Content.ReadAsStringAsync();
            var resultObject = JObject.Parse(result);
            dynamic success = resultObject["success"];
            return (bool)success;
        }
        catch (Exception e)
        {
            // TODO: log this (in elmah.io maybe?)
            return false;
        }
    }
}

The ReCaptcha class is simply a wrapper of a HttpClient and offers a single IsValid method that will validate the token. This is done by posting the token to the reCAPTCHA API and parsing the response. I quickly implemented this with Newtonsoft.Json but I'm sure you already know a better way to parse JSON than me πŸ˜‰

The ReCaptcha class and the internal HttpClient is configured in the ConfigureServices method in Startup.cs:

services.AddHttpClient<ReCaptcha>(x =>
{
    x.BaseAddress = new Uri("https://www.google.com/recaptcha/api/siteverify");
});

The only thing missing now is doing the validation. Inject the ReCaptcha class in the ForgotPassword.cshtml.cs file:

private readonly ReCaptcha _captcha;

public ForgotPasswordModel(/* ... */ ReCaptcha captcha)
{
    // ...
    _captcha = captcha;
}

And validate the token in the OnPostAsync method:

if (ModelState.IsValid)
{
    if (!Request.Form.ContainsKey("g-recaptcha-response")) return Page();
    var captcha = Request.Form["g-recaptcha-response"].ToString();
    if (!await _captcha.IsValid(captcha)) return Page();
    
    // ...
}

Notice that I didn't add the CAPTCHA token to the Input model. Developers with better knowledge about ASP.NET Core model binding would probably bind the token directly to the model. I'm simply getting the g-recaptcha-response form variable attached to the POST request by reCAPTCHA and validating it using the ReCaptcha class. In case of a missing or invalid token, I return the user to the reset password page.

That's it. The form now shows a CAPTCHA control and validates that the user solved it on the backend.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website