Implement Recaptcha properly in ASP.NET MVC

Published on Wednesday, 10 August 2011 by Russ Cam

Love them or hate them, the humble CAPTCHA is a tried and trusted approach to mitigating spam content from infiltrating your beautifully crafted application and flooding it with adverts selling all kinds of nefarious goods. Whilst their effectiveness is debatable and the frustration that they bring about in some high, they are relatively simple to use and catch the bulk of user defined content one would want to filter from their site.

Recaptcha to the rescue

Recaptcha is pretty much the de-facto CAPTCHA implementation and using it helps in digitizing books too! A Nuget package is available that provides the ability to render a CAPTCHA and validate it. It's as simple to use as

@Html.GenerateCaptcha("id", "white")

where “id” is the id of the control to generate and “white” is the theme colour to use. This will render code in the response that results in the following displayed in the browser:

Captcha example

If this is rendered inside of a <form> element, two additional values are posted back to the server; a value keyed against the name “recaptcha_challenge_field” which is an identifier for the CAPTCHA served to the user and a value keyed against the name "recaptcha_response_field" which holds the value that the user entered. These values can be used to validate the user response and are nicely wrapped up  for us in an ActionFilterAttribute:

public class CaptchaValidatorAttribute : ActionFilterAttribute
{
    private const string CHALLENGE_FIELD_KEY = "recaptcha_challenge_field";
    private const string RESPONSE_FIELD_KEY = "recaptcha_response_field";
    private RecaptchaResponse recaptchaResponse;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        RecaptchaValidator recaptchaValidator = new RecaptchaValidator();
        recaptchaValidator.PrivateKey = RecaptchaControlMvc.PrivateKey;
        recaptchaValidator.RemoteIP = filterContext.HttpContext.Request.UserHostAddress;
        recaptchaValidator.Challenge = filterContext.HttpContext.Request.Form["recaptcha_challenge_field"];
        recaptchaValidator.Response = filterContext.HttpContext.Request.Form["recaptcha_response_field"];
        this.recaptchaResponse = !string.IsNullOrEmpty(recaptchaValidator.Challenge) ? (!string.IsNullOrEmpty(recaptchaValidator.Response) ? recaptchaValidator.Validate() : RecaptchaResponse.InvalidResponse) : RecaptchaResponse.InvalidChallenge;
        filterContext.ActionParameters["captchaValid"] = (object)(bool)(this.recaptchaResponse.IsValid ? 1 : 0);
        filterContext.ActionParameters["captchaErrorMessage"] = (object)this.recaptchaResponse.ErrorMessage;
        base.OnActionExecuting(filterContext);
    }
}

We can see that we are given the response and an error message (if the response indicates that what the user entered is invalid or some other error, such as invalid keys) as Action parameters. We can supply two parameters with names matching “captchaValid” and “captchaErrorMessage” to our controller action to capture these values

public class DemoContoller : Controller
{
    [CaptchaValidator]
    public ActionResult Index(PostedModel model, bool captchaValid, string captchaErrorMessage)
    {
        if (!captchaValid)
        {
            // do something to indicate that the user entered the wrong reponse for the CAPTCHA
        }
    }
}

pretty neat and not a lot of effort on our part.

So what’s wrong?

The main problem with the ReCaptcha component as provided is that it will only render a CAPTCHA in response to a full HTTP request i.e. it won’t render a CAPTCHA in the response to a request made with AJAX. If you look deep down in the code the ReCaptcha HtmlHelper extension method generates, it renders a <script> element that makes a request and returns a block of JavaScript used to render the CAPTCHA and provide various settings. The script response looks like so

var RecaptchaState = {
site : '****************************************',
challenge : '03AHJ_VuuAojIMHVd4xlTwTqaPxlDDTHu7dI-GNpOnNMsn9-6QUCtPnDs00KcEUmllKVC8aTM8QnVqczDLB3MxR7QdJSB7GZ45C1vWUz8K9RUczrww4FzAAkpZ8BjbJeKG_-UDusddk-gkJL1NG-b9RurjTDKXmXmtaw',
is_incorrect : false,
programming_error : '',
error_message : '',
server : 'http://www.google.com/recaptcha/api/',
timeout : 18000
};

document.write('<scr'+'ipt type="text/javascript" s'+'rc="' + RecaptchaState.server + 'js/recaptcha.js"></scr'+'ipt>'); 

See the problem? No, it’s not the site id that has been masked, it’s that document.write() call made to write another <script> element into the document. I would highly recommend avoiding document.write() as it can produce different outcomes depending on when it is called. But don’t take my word for it, here’s what Doug Crockford has to say about it:

The document.write method provides a way of incorporating strings into the HTML content of the page. There are better ways to do that, such as .innerHTML and .createElement or HTML cloning patterns. Use of document.write should be avoided.

document.write is recklessly dependent on timing. If document.write is called before the onload event, it appends or inserts text into the page. If it is called after onload, it completely replaces the page, destroying what came before.

document.write encourages bad structure, in which script and markup are intermingled. A cleaner structure has minimal interaction between markup and script.

In this case, the document.write() does not cause the script to be written into the page, whose contents contain the markup to render the CAPTCHA*.

ReCaptcha, Refined

Here is an improved implementation of the ReCaptcha component that will work no matter how the request has been made:

public static class HtmlHelperExtensions
{
    /// <summary>
    /// Renders a ReCaptcha to validate that the user is human
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="id">The id.</param>
    /// <returns>an <see cref="IHtmlString"/> containing the recaptcha</returns>
    public static IHtmlString Captcha(this HtmlHelper helper, string id)
    {
        return Captcha(helper, id, "white");
    }

    /// <summary>
    /// Renders a ReCaptcha to validate that the user is human
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="id">The id.</param>
    /// <param name="theme">The theme.</param>
    /// <returns>an <see cref="IHtmlString"/> containing the recaptcha</returns>
    public static IHtmlString Captcha(this HtmlHelper helper, string id, string theme)
    {
        string captcha;

        // we can't use the standard Captcha if injecting via an AJAX request so 
        // we need to use the AJAX one instead
        if (helper.ViewContext.HttpContext.Request.IsAjaxRequest())
        {
            var builder = new StringBuilder();
            builder.AppendLine("<div id=\"" + id + "\"></div>");
            builder.AppendLine("<script type=\"text/javascript\" src=\"http://www.google.com/recaptcha/api/js/recaptcha_ajax.js\"></script>");
            builder.AppendLine("<script type=\"text/javascript\">");        
            builder.AppendLine("(function() { ");
            builder.AppendLine("var recaptcha_load = setInterval(function() { ");
            builder.AppendLine("if (Recaptcha != undefined) {"); 
            builder.AppendLine("clearInterval(recaptcha_load); Recaptcha.create('" + RecaptchaControlMvc.PublicKey + "', '" + id + "', {");
            builder.AppendLine("theme: '" + theme + "',");
            builder.AppendLine("callback: Recaptcha.focus_response_field");
            builder.AppendLine("}); } }, 10); })();");
            builder.AppendLine("</script>");

            captcha = builder.ToString();
        }
        else
        {
            captcha = helper.GenerateCaptcha(id, theme);
        }
        
        return new HtmlString(captcha);
    }
}

If the request has been made with AJAX, the response returns a <div> element with an id matching the one supplied and two <script> elements; the first <script> element will get JavaScript code that can be used to create a CAPTCHA on the client side, the second <script> element renders code that waits until the first script has been executed (using the global setInterval function) and then creates a CAPTCHA on the client side in the defined <div> element. If the request hasn’t been made with AJAX, the ReCaptcha’s GenerateCaptcha method is called as before. The end result is that a CAPTCHA is rendered no matter how a request is made.

Cleaning up Validation

I’m not too keen on having to specify two additional parameters for an action method that is going to be called including ReCaptcha data. I’d prefer to use ModelState as is used for model property binding and validation, so here is an ActionFilter attribute that allows us to use that:

public sealed class ValidateCaptchaAttribute : ActionFilterAttribute
{
    private const string CHALLENGE_FIELD_KEY = "recaptcha_challenge_field";
    private const string RESPONSE_FIELD_KEY = "recaptcha_response_field";

    public override void OnActionExecuting(ActionExecutingContext filterContext)  
    {  
        var captchaChallengeValue = filterContext.HttpContext.Request.Form[CHALLENGE_FIELD_KEY];  
        var captchaResponseValue = filterContext.HttpContext.Request.Form[RESPONSE_FIELD_KEY];  
        var captchaValidator = new RecaptchaValidator  
        {
          PrivateKey = RecaptchaControlMvc.PrivateKey,  
          RemoteIP = filterContext.HttpContext.Request.UserHostAddress,  
          Challenge = captchaChallengeValue,  
          Response = captchaResponseValue  
        };  
  
        var recaptchaResponse = captchaValidator.Validate();  
  
        if (!recaptchaResponse.IsValid)
        {
            filterContext.Controller.ViewData.ModelState.AddModelError("recaptcha", ValidationResources.InvalidRecaptcha);
        }
  
        base.OnActionExecuting(filterContext);  
    }
}

Now we just need to assess whether ModelState is valid in our controller action:

public class DemoContoller : Controller
{
    [ValidateCaptcha]
    public ActionResult Index(PostedModel model)
    {
        if (!ModelState.IsValid)
        {
            // send the user back to the view
           return View(model);
        }
    }
}

We could optionally render a validation message next to the ReCaptcha too using the model key “recaptcha

@Html.ValidationMessage("recaptcha")
@Html.Captcha("recaptcha")

Hope this helps!

* My testing has been with jQuery to make an AJAX request to return html that contains the markup rendered by the Captcha HtmlHelper. Looking through jQuery 1.6.1 source code, I can see nothing special in there that would cause document.write() to be sanitized in any way, so can only assume that the browser is simply ignoring it as a request is neither being made nor is the document replaced. If you know anything more, please leave a comment!


Comments

comments powered by Disqus