Declaration of VAR

and some other stuff

How to add reCAPTCHA to your .NET Core MVC project

2017-08-23 15:50:04 +0200

2017-08-23 15:50:04 +0200 | Comments

If you have some views with inputs exposed to public access, especially if they are served via controller with [AllowAnonymous], then you most probably would like to add CAPTCHA to those views. Otherwise, you risk to get flooded with automated spam.

I decided to use reCAPTCHA from Google.

Register reCAPTCHA for your domain

Go here and click “Get reCAPTCHA”. Fill the form (with your domain):

After that you’ll see this page:

Here you need the take the following values:

  • Site key - should be place on all views that you want to protect with reCAPTCHA. This value will be used to query reCAPTCHA server when user tries to pass reCAPTCHA;
  • Secret key - this value should never be exposed anywhere except your server. It will be used for checking reCAPTCHA results after user submits the form.

How reCAPTCHA works

How it works: a user must pass the reCAPTCHA test (click on a checkbox or select pictures with trees) before submitting the form. After the form is submitted, your server will receive a g-recaptcha-response value as one of the POST parameters. Then you need to send a HTTP request to https://www.google.com/recaptcha/api/siteverify and pass this value (don’t confuse it with Site key) together with your Secret key .

Here’s a schema of the whole process:

  1. User makes an attempt to pass the reCAPTCHA, and script sends a request to reCAPTCHA server;
  2. If reCAPTCHA was solved, then reCAPTCHA server replies with the user response token (g-recaptcha-response);
  3. After the form is submitted, your server gets this g-recaptcha-response among other POST parameters;
  4. Now you need to query reCAPTCHA server using your Secret key and g-recaptcha-response to see whether user has passed the test or not;
  5. reCAPTCHA server will send you JSON response with results.

And here’s how this JSON response might look like:

{
  "success": true,
  "challenge_ts": "2017-08-22T21:47:03Z",
  "hostname": "example.org"
}

From which you can see whether user has passed the check or not.

Add reCAPTCHA to your project

Put both values (Secret key and Site key) to your appsettings.json:

{
    "GoogleReCaptcha": {
        "key": "YOUR-KEY",
        "secret": "YOUR-SECRET"
    }
}

Add this script to the Scripts section of the view you want to have reCAPTCHA (or maybe into a common layout shared among all such views):

@section Scripts {
    <script src='https://www.google.com/recaptcha/api.js'></script>
}

Add reCAPTCHA’s div to the form:

<div class="g-recaptcha" data-sitekey="@ViewData["ReCaptchaKey"]"></div>

So, now your view might look like this:

@model SomeModel

<form asp-controller="Home" asp-action="Feedback" method="post" class="form-horizontal">
    
    <div asp-validation-summary="All" class="text-danger"></div>

    <div class="form-group">
        <label asp-for="AuthorEmail" class="col-md-2"></label>
        <div class="col-md-10">
            <input asp-for="AuthorEmail" class="form-control" />
            <span asp-validation-for="AuthorEmail" class="text-danger"></span>
        </div>
    </div>
    <div class="form-group">
        <label asp-for="FeedbackMsg" class="col-md-2"></label>
        <div class="col-md-10">
            <textarea asp-for="FeedbackMsg" class="form-control" rows="5"></textarea>
            <span asp-validation-for="FeedbackMsg" class="text-danger"></span>
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-2"></div>
        <div class="col-md-10">
            <div class="g-recaptcha" data-sitekey="@ViewData["ReCaptchaKey"]"></div>
        </div>
    </div>
    
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <button type="submit" class="btn btn-default">Send</button>
        </div>
    </div>
    
</form>

@section Scripts {
    <script src='https://www.google.com/recaptcha/api.js'></script>
}

And that’s the controller for this view:

// A function that checks reCAPTCHA results
// You might want to move it to some common class
public static bool ReCaptchaPassed(string gRecaptchaResponse, string secret, ILogger logger)
{
    HttpClient httpClient = new HttpClient();
    var res = httpClient.GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={secret}&response={gRecaptchaResponse}").Result;
    if (res.StatusCode != HttpStatusCode.OK)
    {
        logger.LogError("Error while sending request to ReCaptcha");
        return false;
    }
    
    string JSONres = res.Content.ReadAsStringAsync().Result;
    dynamic JSONdata = JObject.Parse(JSONres);
    if (JSONdata.success != "true")
    {
        return false;
    }

    return true;
}

[HttpGet("Home/Feedback")]
[AllowAnonymous]
public IActionResult Feedback()
{
    // get reCAPTHCA key from appsettings.json
    ViewData["ReCaptchaKey"] = _configuration.GetSection("GoogleReCaptcha:key").Value;
    return View();
}

[HttpPost("Home/Feedback/")]
[AllowAnonymous]
public IActionResult Feedback(SomeModel model)
{
    // get reCAPTHCA key from appsettings.json
    ViewData["ReCaptchaKey"] = _configuration.GetSection("GoogleReCaptcha:key").Value;

    if (ModelState.IsValid)
    {
        if (!ReCaptchaPassed(
            Request.Form["g-recaptcha-response"], // that's how you get it from the Request object
            _configuration.GetSection("GoogleReCaptcha:secret").Value,
            _logger
            ))
        {
            ModelState.AddModelError(string.Empty, "You failed the CAPTCHA, stupid robot. Go play some 1x1 on SFs instead.");
            return View(model);
        }

        // do your stuff with the model
        // ...

        return View();
    }

    return View(model);
}

That’s it.

And while I was looking for how to do it, I saw all sorts of crazy-rocket-science-complicated solutions like creating custom controller attributes / tag helpers, injecting some special access to HttpContext, or even using a NuGet package (there are NuGet packages for this!). And all that just for getting one POST parameter out of the Request object?

I mean, how complicated can that be?