Asp.net mvc captcha validation made easy

When I was working on this blog a few weeks ago I realized that I might need a captcha of some sort for my comments and for the contact page. I figured that I would probably need to use it for a few more projects in the future, so I decided to try and create some kind of generic way of creating and validating captchas and put it in my code base library.

After reading a few blogs on the subject I came up with a pretty neat idee to have a captcha method on a basecontroller that returns a ImageStreamResult, which is a ActionResult that saves a bitmap to the output stream. Because I want this to be as generic and usefull as possible, I decided to have a interface "ICaptchaGenerator" to make it easy to cange the image generator if I want to. I allso made a default generator if I dont want to supply a custom generator. The interface looks like this:

public interface ICaptchaGenerator
{
    Bitmap GenerateCaptcha(string captchaText, int width, 
                           int height);
}

 To be able to validate the captcha against the input I decided to generate a guid when I display the image and the cache the value with the guid as key. To display the captcha image I created a html helper that takes a Func<Guid, Expression<Action<TController>>> func as argument to point at the action that generates the captcha image. The helper generates the guid and put it in the url for the action to handle it. The I render a image and a hidden field with the guid. The helper looks like this:

public static string CaptchaValidation<TController>(
                      this HtmlHelper helper, 
                      Func<Guid, 
                      Expression<Action<TController>>> func, 
                      object htmlAttributes)
                      where TController : Controller
{
    var guid = Guid.NewGuid();
    var routeValues = ExpressionHelper.
                            GetRouteValuesFromExpression(
                            func.Invoke(guid));
    var url = new UrlHelper(helper.ViewContext.RequestContext, 
                    helper.RouteCollection).RouteUrl(routeValues);

    var sb = new StringBuilder();
    sb.AppendFormat("<img src=\"{0}\" alt=\"\" />", url);
    sb.Append(helper.Hidden("captcha-guid", guid));
    return sb.ToString();
}

Now I have a way of creating and displaying a captcha image. The next step is to validate it, to be sure the user entered the right combination. I wanted to make this as easy as possible as I have putting to much code in my controller action. So I decided to have a attribute that I can set on all actions that should walidate a captcha input, and do the validation in my baescontroller. The attribute I use looks like this:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, 
Inherited = false)]
public sealed class CaptchaValidationAttribute : Attribute
{
    public CaptchaValidationAttribute() 
        : this("Captcha not valid", "captcha") { }

    public CaptchaValidationAttribute(string message)
        : this(message, "captcha")
    {
        ErrorMessage = message;
    }

    public CaptchaValidationAttribute(string message, string field)
    {
        ErrorMessage = message;
        Field = field;
    }

    public string Field { get; private set; }
    public string ErrorMessage { get; private set; }
}

The attribute simply takes a message to display if the captcha is invalid and the name of the text input field. The real "magic" then happens in the OnActionExecuting mehod on my base controller. Here I check if we have a CaptchaValidationAttribute present on action that is being executed. If we do I check the value of the field against the cached value. If they match I dont do anything, but if they dont I simply add a model error to the ModelState. This makes it very easy to use this kind of validation. The only think I need to do is add the attribute to the action and check if the modelstate is vaild before I insert anything. 

I can then use it like this:

[CaptchaValidation("Invalid captcha", "txtCaptcha")]
public ActionResult Save([Bind(Exclude = "Children,Added,Item")]
                         Comment comment)
{
    if (ModelState.IsValid)
        _commentService.AddComment(comment);
    //Action logic
}

And on the view:

<%=Html.CaptchaValidation<CommentController>(x => y => 
                                   y.CaptchImage(x)) %>
<input id="txtCaptcha" type="text" name="txtCaptcha" />

I´m sure there is alot of stuff I can improve here. After all, this is just the first example. But it works pretty good for me so far. And feel free to give me sugestions and feedback, and tell me how you would have done it.

I have uploaded the source here for you to download.

kick it on DotNetKicks.com
Shout it

Comments

Add comment

Name:

Mail:

Website:

Comment:


<< [1] >>
<< [1] >>