When I decided to add recaptcha, to an email contact form, I naturally started looking online at options for completing the task. There were many routes I could have gone, but it seemed that most of the blog sources out there all pointed me to a nuget package, or to “go to codeplex and get this thing” . Then, when reviewing the documentation for those ‘helper’ projects, all of the processes seemed convoluted, and disappointing. I knew there had to be a simple, and pure, approach and so I decided to go to the source: Google.
Google is great at many things. I generally think their documentation is solid as well, but honestly, the docs for recaptcha are fairly weak. They contain all of the needed information, but in a way that is only slightly clearer than mud.
However, using what they gave me, and some creativity, I was able to arrive at a working solution. I will explain it all below:
Inserting the Captcha
First, as Google instructs you to do, add the following html where you want the captcha to appear. Along with the JavaScript (at the end of your html body). Note: Pay attention to the data-callback attribute that is not shown in Google’s example, but described in their surrounding text.
1 2 3 4 5 6 7 8 9 |
<div> <!-- I want the recaptcha to appear here --> <div class="g-recaptcha" data-sitekey="your_site_key" data--callback="handleCaptcha"></div> </div> <!-- and a the very bottom of the page body --> <script src="https://www.google.com/recaptcha/api.js" async defer></script> |
So that will actually show you the captcha if you try it just as it is. However, we have to validate it as well.
Working with the Captcha Client Side
In order to do that you really have a two step process. When the user interacts, the callback method you specify in that data-callback attribute is triggered. However, the value is simply an encoded response, and you will pass it back to the server as-is. To best accommodate this, I added a @Html.Hidden field to my page, and mapped it to a string property in my view model. Then, using the handleCaptcha() call back method, I set the response from Google into that hidden field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<div class="row"> @Html.HiddenFor(m => m.RecaptchaValue, new { @class="recaptcha"}) <p>No offense, but please prove you are a human -- or at least a well written bot.</p> <div class="g-recaptcha" data-sitekey="@Model.RecaptchaSiteKey" data-callback="handleCaptcha"></div> </div> <!-- This is all inside the body, but at the very end of the body --> <script src="https://www.google.com/recaptcha/api.js" async defer></script> <script type="text/javascript"> function handleCaptcha(e) { $(".recaptcha").val(e); } </script> |
So now you can present the captcha to the user, and capture the response contained in the call back, and send it to your controller for further validation. For this final validation you make a final api call (as described by Google) to get a yes or no answer.
Bringing it all back to the server to validate securely
For that I simply added a subroutine that returns a boolean, and call that in my controller passing the Google response string. Note: The response from the API call is a JSON object that .NET deserializes to a hashtable (technically a dictionary<string, object>). You find the “success key” and its value is your boolean success indicator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public ActionResult ContactSubmit(ContactModel model) { ActionResult redirectOrReturn = null; // See if the captcha was valid before anything, // then once we've added the error (if it's wrong) // we can check the model state. if (!ValidateCaptcha(model.RecaptchaValue)) { ModelState.AddModelError("recaptcha", "Captcha response was not validated."); } if (ModelState.IsValid) { // Do your thing, whatever it is } } private bool ValidateCaptcha(string value) { bool response = false; string googleApiUrl = "https://www.google.com/recaptcha/api/siteverify?" + "secret={0}&response={1}&remoteip={2}"; string ip = Request.UserHostAddress; string secretKey = System.Configuration.ConfigurationManager .AppSettings.Get("RecaptchaSecretKey"); HttpWebRequest r = (HttpWebRequest)HttpWebRequest.Create( string.Format(googleApiUrl, secretKey, value, ip)); HttpWebResponse p = (HttpWebResponse)r.GetResponse(); using (var reader = new StreamReader(p.GetResponseStream())) { JavaScriptSerializer js = new JavaScriptSerializer(); Dictionary<string, object> obj = js.Deserialize<dynamic>( reader.ReadToEnd()); foreach (var o in obj) { if (o.Key == "success") { response = (bool)o.Value; } } } return response; } |
That was it, it works exactly as I would have expected, and is quick and painless. I also added logic to disable my submit button until the captcha callback took place, that way the user couldn’t accidentally submit the form without that response.