How to add reCAPTCHA to your .NET Core MVC project
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:
- User makes an attempt to pass the reCAPTCHA, and script sends a request to reCAPTCHA server;
- If reCAPTCHA was solved, then reCAPTCHA server replies with the user response token (
g-recaptcha-response
); - After the form is submitted, your server gets this
g-recaptcha-response
among other POST parameters; - Now you need to query reCAPTCHA server using your
Secret key
andg-recaptcha-response
to see whether user has passed the test or not; - 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?
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks