Robots gotcha down? Get reCAPTCHA in ASP.NET MVC.

So it seems that the battle between sites and bots is never ending.  On the good side we have a proven warrior, CAPTCHA, invented by Captain John Cha, a decorated war hero in the french canadian robot war of 1983. 

robots

I’m not gonna go into real details about when and where you should use CAPTCHA controls, but in general it’s probably a good idea on any form that has the potential to be misused or abused by pesky bots or pesky non-bots (usually human).  I like reCAPTCHA because its free and it makes me feel all warm inside that I’m helping a greater cause.

reCAPTCHA improves the process of digitizing books by sending words that cannot be read by computers to the Web in the form of CAPTCHAs for humans to decipher. More specifically, each word that cannot be read correctly by OCR is placed on an image and used as a CAPTCHA. This is possible because most OCR programs alert you when a word cannot be read correctly.

 

It’s pretty dead simple to get this thing going in MVC, as with most things in MVC, it’s just a different way of thinking instead of having a server (or user) control its probably best to implement it as an html helper.  Keep in mind there are a good amount of ways to do this and there are ways to customize the display of the CAPTCHA that I’m not going to get into.  Here’s a short list of things to do to get you started.

  • Sign up for the free account with reCAPTCHA.
  • Make sure you get your private and public keys.
  • Get all fancy and create a settings class to handle all of your config options.

     

/// <summary>
/// Settings for configuring reCAPTCHA
/// </summary>
public class reCAPTCHASection : ConfigurationSection
{
    public const string VerifyUrlKey = "VerifyUrl";
    public const string InsecureHostKey = "InsecureHost";
    public const string SecureHostKey = "SecureHost";
    public const string PrivateKeyKey = "PrivateKey";
    public const string PublicKeyKey = "PublicKey";
    public const string ChallengeInputNameKey = "ChallengeInputName";
    public const string ResponseInputNameKey = "ResponseInputName";
    public const string ThemeKey = "Theme";

    [ConfigurationProperty(VerifyUrlKey, DefaultValue = "http://api-verify.recaptcha.net/verify"),
    Description("The url used to verify the challenge")]
    public string VerifyUrl
    {
        get { return this[VerifyUrlKey] as string; }
        set { this[VerifyUrlKey] = value; }
    }

    [ConfigurationProperty(InsecureHostKey, DefaultValue = "http://api.recaptcha.net"),
   Description("The url used retrieve the CAPTCHA when not using ssl")]
    public string InsecureHost
    {
        get { return this[InsecureHostKey] as string; }
        set { this[InsecureHostKey] = value; }
    }


    [ConfigurationProperty(SecureHostKey, DefaultValue = "https://api-secure.recaptcha.net"),
   Description("The url used retrieve the CAPTCHA when using ssl")]
    public string SecureHost
    {
        get { return this[SecureHostKey] as string; }
        set { this[SecureHostKey] = value; }
    }

    [ConfigurationProperty(PrivateKeyKey, DefaultValue = "YourPrivateKey"),
Description("Private key used for validation")]
    public string PrivateKey
    {
        get { return this[PrivateKeyKey] as string; }
        set { this[PrivateKeyKey] = value; }
    }

    [ConfigurationProperty(PublicKeyKey, DefaultValue = "YourPublicKey"),
  Description("Public key used for validation")]
    public string PublicKey
    {
        get { return this[PublicKeyKey] as string; }
        set { this[PublicKeyKey] = value; }
    }

    [ConfigurationProperty(ResponseInputNameKey, DefaultValue = "recaptcha_response_field"),
 Description("Response field input name")]
    public string ResponseInputName
    {
        get { return this[ResponseInputNameKey] as string; }
        set { this[ResponseInputNameKey] = value; }
    }

    [ConfigurationProperty(ChallengeInputNameKey, DefaultValue = "recaptcha_challenge_field"),
Description("Challenge field input name")]
    public string ChallengeInputName
    {
        get { return this[ChallengeInputNameKey] as string; }
        set { this[ChallengeInputNameKey] = value; }
    }

    [ConfigurationProperty(ThemeKey, DefaultValue = "clean"),
Description("Theme (red, white, blackglass, clean, custom")]
    public string Theme
    {
        get { return this[ThemeKey] as string; }
        set { this[ThemeKey] = value; }
    }
}
  • Encapsulate the formatting and validation logic into its own class. Note that I’m using an HttpForm class to wrap the actual calls to the validation service. You will just need to create a WebRequest and execute a post. Most of this code is not unique and was either taken from some of the open source ASP.NET reCAPTCHA controls or other projects.
/// <summary>
/// Encapsulates the recaptcha logic
/// </summary>
public class reCAPTCHA
{

    private readonly reCAPTCHASection settings;
    private IHttpForm httpForm;

    public reCAPTCHA(reCAPTCHASection settings, IHttpForm httpForm){
        Check.Argument.IsNotNull(settings, "settings");
        Check.Argument.IsNotNull(httpForm, "httpForm");
        this.httpForm = httpForm;
        this.settings = settings;
        
    }

    /// <summary>
    /// Generates the HTML.
    /// </summary>
    /// <param name="isSecure">if set to <c>true</c> [is secure].</param>
    /// <returns></returns>
    public string GenerateHtml(bool isSecure)
    {
        string result = "";
        using (var tmpWriter = new StringWriter())
        {
            using (var writer = new HtmlTextWriter(tmpWriter))
            {

                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
                writer.RenderBeginTag(HtmlTextWriterTag.Script);
                writer.WriteLine("var RecaptchaOptions = {");
                writer.WriteLine("theme : '{0}'".FormatWith(settings.Theme ?? string.Empty));
                writer.WriteLine("};");
                writer.RenderEndTag();

                // <script> display
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
                writer.AddAttribute(HtmlTextWriterAttribute.Src, GenerateChallengeUrl(isSecure,false), false);
                writer.RenderBeginTag(HtmlTextWriterTag.Script);
                writer.RenderEndTag();

                writer.RenderBeginTag(HtmlTextWriterTag.Noscript);

                writer.AddAttribute(HtmlTextWriterAttribute.Src, GenerateChallengeUrl(isSecure, true), false);
                writer.AddAttribute(HtmlTextWriterAttribute.Width, "500");
                writer.AddAttribute(HtmlTextWriterAttribute.Height, "300");
                writer.AddAttribute("frameborder", "0");
                writer.RenderBeginTag(HtmlTextWriterTag.Iframe);
                writer.RenderEndTag();

                writer.RenderBeginTag(HtmlTextWriterTag.Br);
                writer.RenderEndTag();

                writer.AddAttribute(HtmlTextWriterAttribute.Name, settings.ChallengeInputName);
                writer.AddAttribute(HtmlTextWriterAttribute.Rows, "3");
                writer.AddAttribute(HtmlTextWriterAttribute.Cols, "40");
                writer.RenderBeginTag(HtmlTextWriterTag.Textarea);
                writer.RenderEndTag();

                writer.AddAttribute(HtmlTextWriterAttribute.Name, settings.ResponseInputName);
                writer.AddAttribute(HtmlTextWriterAttribute.Value, "manual_challenge");
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
                writer.RenderBeginTag(HtmlTextWriterTag.Input);
                writer.RenderEndTag();

                writer.RenderEndTag();
                result = tmpWriter.ToString();
            }
        }
        return result;
    }

    

    /// <summary>
    /// Validates the CAPTCHA challenge.
    /// </summary>
    /// <param name="fromIpAddress">From ip address.</param>
    /// <param name="challenge">The challenge.</param>
    /// <param name="response">The response.</param>
    /// <returns></returns>
    public virtual bool Validate( string fromIpAddress, string challenge, string response){
        Check.Argument.IsNotEmpty(fromIpAddress, "fromIPAddress");
        Check.Argument.IsNotEmpty(challenge, "challenge");
        Check.Argument.IsNotEmpty(response, "response");

        try{
            var fields = new NameValueCollection{
                                                        {
                                                                "privatekey",
                                                                settings.PrivateKey.
                                                                UrlEncode()
                                                                },
                                                        {
                                                                "remoteip",
                                                                fromIpAddress
                                                                .UrlEncode()
                                                                },
                                                        {
                                                                "challenge",
                                                                challenge.
                                                                UrlEncode()
                                                                },
                                                        {
                                                                "response",
                                                                response.
                                                                UrlEncode()
                                                                }
                                                };
            string[] result = httpForm.Post(
                    new HttpFormPostRequest
                    {
                        Url = settings.VerifyUrl,
                        FormFields = fields
                    }
                    ).Response.Split();

            if (result.Length > 0){
                bool isValid;

                if (!bool.TryParse(result[0], out isValid)){
                    isValid = false;
                }

                return isValid;
            }
        }
        catch (WebException e){
            Log.Exception(e);
        }

        return true;
    }

    private string GenerateChallengeUrl(bool isSecure, bool noScript)
    {
        var urlBuilder = new StringBuilder();

        urlBuilder.Append(isSecure ? settings.SecureHost : settings.InsecureHost);

        urlBuilder.Append(noScript ? "/noscript?" : "/challenge?");
        urlBuilder.AppendFormat("k={0}", settings.PublicKey);
        return urlBuilder.ToString();
    }
}
  • You’ll notice that the generateHTML method is actually just creating the recommended html from their website which includes a noscript tag.
  • Now for the MVC part create a extension method for HTML Helper. Yes there is a little structuremap going on here, you can ignore it since you aren’t using the HTTPForm abstraction class.
/// <summary>
/// Displays the CAPTCHA.
/// </summary>
/// <param name="helper">The helper.</param>
/// <param name="settings">The settings.</param>
/// <param name="isSecure">if set to <c>true</c> [is secure].</param>
/// <returns></returns>
public static string DisplayCAPTCHA(this HtmlHelper helper, reCAPTCHASection settings, bool isSecure){

    var captcha = new reCAPTCHA(settings, ObjectFactory.GetInstance<IHttpForm>());
    return captcha.GenerateHtml(isSecure);

   
}
  • On your view page put call this code inside of your form tags.
  • Finally on your controller expect 2 form fields to be submitted and you’ll end up doing something like the following…
          string captchaChallenge = null;
               string captchaResponse = null;
               var settings = new reCAPTCHASection();
              
               model.Validate();

               captchaChallenge = HttpContext.Request.Form[settings.ChallengeInputName];
               captchaResponse = HttpContext.Request.Form[settings.ResponseInputName];
               var rval = captcha.Validate(CurrentUserIPAddress, captchaChallenge, captchaResponse); 

 

Obviously you’ll need to do something with that bool that you get back, you may decide to use the ModelState to return an error or redirect them to some other page.  I apologize for not extracting some of the injection stuff and the HTTPForm class you will either need to create or just call WebRequest directly.  I didn’t want to get into how to make a post request.  Hope this helps you to get started in the right direction. 

 

kick it on DotNetKicks.com
Bookmark and Share
blog comments powered by Disqus
  • Menu

  • Tags