coderberry

Roll Your Own Ajax-Based Captcha in Grails

Recently, I was asked to come up with a better solution to our captcha needs. We have been using ReCaptcha, which is great but difficult to read at times, and has caused frustrated customers and lost sales. I found a great solution at http://www.jkdesign.org/captcha which displays a number of graphics and let’s the user choose the right one to prove they are human. Here is a screenshot of my implementation:

To make this work within Grails, I had to make several tweaks. The following files are required:

Create a new controller called Captcha. This can really be named anything, but if you do rename it, it will have to be updated in the jquery.simpleCaptcha-0.2.js file or passed in as an option via the javascript.

CaptchaConverter.groovy
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
51
52
53
54
55
56
57
58
59
60
61
package com.berry

import com.berry.BCrypt
import grails.converters.JSON

class CaptchaController {

    def index = {

        // Generate the SALT to be used for encryption and place in session
        def captchaSalt = session.captchaSalt ?: BCrypt.gensalt()
        session.selectedCaptchaText = null
        session.captchaSalt = captchaSalt

        // Modify below for custom images
        def images = [
                'house':        'images/captchaImages/01.png',
                'key':          'images/captchaImages/04.png',
                'flag':         'images/captchaImages/06.png',
                'clock':        'images/captchaImages/15.png',
                'bug':          'images/captchaImages/16.png',
                'pen':          'images/captchaImages/19.png',
                'light bulb':   'images/captchaImages/21.png',
                'musical note': 'images/captchaImages/40.png',
                'heart':        'images/captchaImages/43.png',
                'world':        'images/captchaImages/99.png'
        ]

        // Create the image array to be returned in JSON format
        def size = images.size()
        def num = Math.min(params.numImages ? params.int('numImages') : 5, size)
        def keys = images.keySet().toList()
        def used = []
        def random = new Random()
        (1..num).each { i ->
            def idx = random.nextInt(keys.size())
            def item = keys.get(idx)
            keys.remove(idx)
            used << item
        }

        // Select the 'chosen' text to be used for authentication and place in session
        def selectedText = used[random.nextInt(used.size())]
        def hashedSelectedText = BCrypt.hashpw(selectedText, captchaSalt);
        session.selectedCaptchaText = hashedSelectedText

//        println "SELECTED: ${hashedSelectedText}"
//        println "USED: ${used.inspect()}"

        // Generate object to be returned
        def ret = [
                text: selectedText,
                images: []
        ]
        used.each { u ->
            ret['images'] << [hash: BCrypt.hashpw(u, captchaSalt), file: images[u]]
        }

        render ret as JSON
    }
}

What this controller does is return a JSON object with the data needed to generate the captcha. The JSON appears like so:

data.json
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
{
  "text":"heart",
  "images":[
    {
      "hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2Oo.E3D8FCzha2772XuXm7v28Kx2LNL5S",
      "file":"images/captchaImages/99.png"
    },
    {
      "hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2Oa5Y/I/cXOUj30kffPqyX0qxTnAACX6O",
      "file":"images/captchaImages/43.png"
    },
    {
      "hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2O8zeOa4.ed1s8pZS9AgkalcSSQm9pmbi",
      "file":"images/captchaImages/15.png"
    },
    {
      "hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2OSNYwC4RPwhNpuPYBbeNB0j4ozoItwDK",
      "file":"images/captchaImages/06.png"
    },
    {
      "hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2OLv6DzHHDX0aB2AwS1YEVZMp9cEpo2sq",
      "file":"images/captchaImages/01.png"
    }
  ]
}

Now we just need to implement this in our GSP file and controller. Suppose we have a page like shown above with a pickup code and the last 4 digits of the persons phone number. With adding our captcha div and required javascript, our GSP would look like this:

view.gsp
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
51
52
53
54
55
56
57
58
59
<!-- PLACE IN HEADER -->
<script type="text/javascript" src="${resource(dir:'js',file:'jquery.simpleCaptcha-0.2.js')}"></script>
<style type="text/css">
    img.simpleCaptcha {
        margin: 2px !important;
        cursor: pointer;
    }
    img.simpleCaptchaSelected {
        margin-bottom: 0px;
        border-bottom: 2px solid red;
    }
</style>

<!-- BODY CONTENTS -->
<g:form action="pickup">

    <div class="stylized myform" style="width:542px;">
        <h2>Your pickup code will be given to you by your loan consultant</h2>

        <g:if test="${flash.message}">
            <div class="error">
                ${flash.message}
            </div>
        </g:if>

        <div class="clearfix formField">
            <label class="label_only">Pickup Code</label>
            <g:textField name="pickupCode" value="${pickupCode}" autocomplete="no" class="text" />
        </div>
        <div class="clearfix formField">
            <label class="label_only">Last 4 Digits of Phone</label>
            <span class="after_checkbox" style="padding-right: 0px;">(XXX) XXX-</span>
            <g:textField name="lastFourDigits" value="${lastFourDigits}" autocomplete="no" class="text" maxLength="4" />
        </div>
        <div class="clearfix formField">
            <label class="label_only">Are You Human?</label>
            <div style="float: left; margin-left: 10px;">
              <!-- Begin Captcha -->
                <div id="captcha"></div>
              <!-- End Captcha -->
            </div>
        </div>
        <div class="clearfix" style="margin-top: 15px;">
            <label class="label_only">&nbsp;</label>
            <g:submitButton name="submit" value="Show Me My Offer" class="button" />
        </div>

    </div>

</g:form>

<script type="text/javascript">
    $(document).ready(function() {
        $('#captcha').simpleCaptcha({
            numImages: 5,
            introText: '<p>Are you human? Then pick out the <strong class="captchaText"></strong></p>'
        });
    });
</script>

Finally, we need to perform the validation on the controller side. The modified authentication action would look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
def pickup = {

    // Determine if the captcha is picked correctly
    if (params.captchaSelection != session.selectedCaptchaText) {

      // They selected the correct Captcha image. Continue with Authentication

    } else {
        flash.message = "You did not click the correct image below. Please Try Again."
    }

}

So there ya go. It’s actually pretty easy and customers seem to like choosing an image much more than typing a word that is difficult to read.

Comments