Saturday, 14 July, 2018 UTC


Summary

CAPTCHA (/kæp.tʃə/, an acronym for “Completely Automated Public Turing test to tell Computers and Humans Apart”) is a type of challenge–response test used in computing to determine whether or not the user is human.[1]
The term was coined in 2003 by Luis von Ahn, Manuel Blum, Nicholas J. Hopper, and John Langford.[2] The most common type of CAPTCHA was first invented in 1997 by two groups working in parallel: (1) Mark D. Lillibridge, Martin Abadi, Krishna Bharat, and Andrei Z. Broder; and (2) Eran Reshef, Gili Raanan and Eilon Solan.[3] This form of CAPTCHA requires that the user type the letters of a distorted image, sometimes with the addition of an obscured sequence of letters or digits that appears on the screen. Because the test is administered by a computer, in contrast to the standard Turing test that is administered by a human, a CAPTCHA is sometimes described as a reverse Turing test.
This user identification procedure has received many criticisms, especially from disabled people, but also from other people who feel that their everyday work is slowed down by distorted words that are difficult to read. It takes the average person approximately 10 seconds to solve a typical CAPTCHA.[4]
Demo
On the similar lines, we are going to build a simple Captcha, which is going to validate a form before submitting it. Here is how it is going to look like
Live demo of the same can be found here: arvindr21.github.io/captcha
So, let’s get started.
Design
The Captcha module that we are going to create in this post can be tightly coupled with an existing form or can be used in a generic way.
We are going to have a container that the app owner is going to provide. The captcha and the input field are going to be created inside this container.
To generate the passcode, we are going to use 
Math.random()
 in combination with a computed scenario of uppercase or lowercase letters.
The input field will be publishing two events on keyup. 
captcha.success
 when the validation of text box value matches the generated passcode & 
captcha.failed
 when the values do not match. The “form owner” is going to listen to these events and work with it to get to know if the captcha is valid or invalid.
In the sample form we are building, we are going to enable or disable the submit button based on the above events.
And we are going to style the text in canvas as well to add some randomness.
So, let’s get started.
Setup Simple Login Form
We are first going to setup a simple login form using Twitter Bootstrap 4. We are going to use Signin Template for Bootstrap for the same.
Anywhere on your machine, create a folder named 
login-form-with-captcha
 and inside that create a new file named 
index.html
.
Update 
index.html
  as shown below
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="Building a Captcha using HTML5 Canvas">
    <meta name="author" content="Arvind Ravulavaru">
    <title>Captcha Login</title>
    <!-- Bootstrap core CSS -->
    <link href="https://getbootstrap.com/docs/4.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Custom styles for this template -->
    <link href="https://getbootstrap.com/docs/4.1/examples/sign-in/signin.css" rel="stylesheet">
</head>

<body>
    <form class="form-signin">
        <h1 class="h3 mb-3 font-weight-normal text-center">Login</h1>
        <label for="inputEmail" class="sr-only">Email address</label>
        <input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" id="inputPassword" class="form-control" placeholder="Password" required>
        <button class="btn btn-lg btn-primary btn-block" disabled="" type="submit">Sign in</button>
    </form>
</body>

</html>
I have borrowed the structure and styles from Signin Template for Bootstrap and built the above page.
As you can see, the Sign in button is disabled on load. We will enable the button once the Captcha is valid. We will do this in the later sections.
If you open the page in the browser, it should look something like

A simple form for a simple job!
Setup Captcha
Now, we are going to setup the container where the Captcha is going to appear. Just above the button tag,  we will add the Captcha container
// snipp snipp
<div id="captcha" class="mb-3 text-center"></div>
// snipp snipp
Next, we are going to create a file named 
captcha.js
 in the 
login-form-with-captcha
folder and then we will link the same to 
index.html
  just after the
form
 tag as shown below
// snipp snipp
<script src="catpcha.js"></script>
// snipp snipp
The basic 
captcha.js
 would be structured this way
// https://stackoverflow.com/a/23925102/1015046
! function() {
    const Captcha = {
        init: function() {
            console.log('Captcha Init Success!')
        }
    }

    // Init Captcha
    Captcha.init();
}();
The above code is an IIFE with a Module pattern.
If we save the above file and refresh the browser, we should see the following in the console
Now, we will flesh out the remaining code.
Develop Captcha module
Now that we have a basic structure, we will continue building the plugin. First we are going to setup the 
init()

Init()

In 
init()
, we are going to do the following
  1. Get the captcha container
  2. If container does not exists, exit the initialisation
  3. Create a canvas element & an input text element
  4. Setup Canvas
  5. Setup Textbox
  6. Bind events on Textbox
  7. Generate Passcode
  8. Set Passcode
  9. Render component on page
The Updated 
init()
 would look like
init: function() {
    this.captchaCntr = document.getElementById('captcha');
    // No Container No Captcha
    if (!this.captchaCntr) {
        return;
    }

    this.canvas = document.createElement('canvas');
    this.input = document.createElement('input');

    this.setupCanvas();
    this.setupTB();

    this.generatePasscode();
    this.setPasscode();

    this.render();
}

setupCanvas()

We are going to setup a canvas element as shown below
setupCanvas: function() {
    this.ctx = this.canvas.getContext('2d');
    this.canvas.width = this.captchaCntr.clientWidth;
    this.canvas.height = 75;
},
We are setting up a canvas with the width of the parent container and height of 75px.

setupTB()

The text box will be setup as shown below
setupTB: function() {
    this.input.type = 'text';
    this.input.className = 'form-control';
    this.input.placeholder = 'Enter Captcha';
    this.bindTBEvents();
},
We have updated the type of input field and added a className and a placeholder. Next, we are going to bind events to the input field. The .are the events that the parent application consuming this captcha would be listening to.

bindTBEvents()

On keyup, we are going to validate the passcode against the input field
bindTBEvents: function() {
    const that = this;
    const successEvt = new Event('captcha.success');
    const failedEvt = new Event('captcha.failed');

    this.input.onkeyup = function() {
        if (this.value === that.passcode) {
            that.captchaCntr.dispatchEvent(successEvt);
        } else {
            that.captchaCntr.dispatchEvent(failedEvt);
        }
    }
},
We have created 2 events
  1. When the validation is successful
  2. When the validation failed
Pretty straight forward.

generatePasscode()

Below is the logic to generate a random passcode.
generatePasscode: function() {
    // https://stackoverflow.com/a/8084248/1015046
    const passcode = Math.random().toString(36).substring(7);
    // If epoch timestamp is even, we convert all the alphabets
    // in string to lowercase, else uppercase. - Some air of mystery
    // https://stackoverflow.com/a/36056672/1015046
    this.passcode = (+new Date()) % 2 === 0 ? passcode.toLowerCase() : passcode.toUpperCase();
},
Scripting to the rescue!

setPasscode()

To 
setPasscode()
 , we are going to add two more helper functions that create a random font color for a character.
randomColor: function() {
    // https://stackoverflow.com/a/7665485/1015046
    var r = Math.floor(Math.random() * 256);
    var g = Math.floor(Math.random() * 256);
    var b = Math.floor(Math.random() * 256);
    return "rgb(" + r + "," + g + "," + b + ")";
},

skinText: function(str, x, y) {
    // https://stackoverflow.com/a/7665485/1015046
    for (var i = 0; i <= str.length; ++i) {
        var ch = str.charAt(i);
        this.ctx.fillStyle = this.randomColor();
        this.ctx.fillText(ch, x, y);
        x += this.ctx.measureText(ch).width;
    }
}
And then finally
setPasscode: function() {
    this.ctx.font = '48px serif';
    this.skinText(this.passcode, 50, 50);
},

render()

Now that we have all the pieces ready, we will render the canvas and text box on the page
render: function() {
    this.captchaCntr.appendChild(this.canvas);
    this.captchaCntr.appendChild(this.input);
}
This concludes our Captcha module.
The final 
catpcha.js
 would be as follows
/**
 * MIT License https://opensource.org/licenses/mit-license.php
 * Author Arvind Ravulavaru
 */

// https://stackoverflow.com/a/23925102/1015046
! function() {
    // https://caniuse.com/#search=const
    const Captcha = {
        init: function() {
            this.captchaCntr = document.getElementById('captcha');
            // No Container No Captcha
            if (!this.captchaCntr) {
                return;
            }

            this.canvas = document.createElement('canvas');
            this.input = document.createElement('input');

            this.setupCanvas();
            this.setupTB();

            this.generatePasscode();
            this.setPasscode();

            this.render();
        },
        setupCanvas: function() {
            this.ctx = this.canvas.getContext('2d');
            this.canvas.width = this.captchaCntr.clientWidth;
            this.canvas.height = 75;
        },
        setupTB: function() {
            this.input.type = 'text';
            this.input.className = 'form-control';
            this.input.placeholder = 'Enter Captcha';
            this.bindTBEvents();
        },
        bindTBEvents: function() {
            const that = this;
            const successEvt = new Event('captcha.success');
            const failedEvt = new Event('captcha.failed');

            this.input.onkeyup = function() {
                if (this.value === that.passcode) {
                    that.captchaCntr.dispatchEvent(successEvt);
                } else {
                    that.captchaCntr.dispatchEvent(failedEvt);
                }
            }
        },
        generatePasscode: function() {
            // https://stackoverflow.com/a/8084248/1015046
            const passcode = Math.random().toString(36).substring(7);
            // If epoch timestamp is even, we convert all the alphabets
            // in string to lowercase, else uppercase. - Some air of mystery
            // https://stackoverflow.com/a/36056672/1015046
            this.passcode = (+new Date()) % 2 === 0 ? passcode.toLowerCase() : passcode.toUpperCase();
        },
        randomColor: function() {
            // https://stackoverflow.com/a/7665485/1015046
            var r = Math.floor(Math.random() * 256);
            var g = Math.floor(Math.random() * 256);
            var b = Math.floor(Math.random() * 256);
            return "rgb(" + r + "," + g + "," + b + ")";
        },
        skinText: function(str, x, y) {
            // https://stackoverflow.com/a/7665485/1015046
            for (var i = 0; i <= str.length; ++i) {
                var ch = str.charAt(i);
                this.ctx.fillStyle = this.randomColor();
                this.ctx.fillText(ch, x, y);
                x += this.ctx.measureText(ch).width;
            }
        },
        setPasscode: function() {
            this.ctx.font = '48px serif';
            this.skinText(this.passcode, 50, 50);
        },
        render: function() {
            this.captchaCntr.appendChild(this.canvas);
            this.captchaCntr.appendChild(this.input);
        }
    }

    // Init Captcha
    Captcha.init();
}();
Setup Validation
Now, we are going to setup the validation on the form. After including 
catpcha.js
 in the 
index.html
<script type="text/javascript">
const button = document.getElementsByTagName('button')[0];
const captcha = document.getElementById('captcha');

captcha.addEventListener('captcha.success', function() {
    button.disabled = false;
});

captcha.addEventListener('captcha.failed', function() {
    button.disabled = true;
});
</script>
The complete 
index.html
 would be as follows
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="Building a Captcha using HTML5 Canvas">
    <meta name="author" content="Arvind Ravulavaru">
    <title>Captcha Login</title>
    <!-- Bootstrap core CSS -->
    <link href="https://getbootstrap.com/docs/4.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Custom styles for this template -->
    <link href="https://getbootstrap.com/docs/4.1/examples/sign-in/signin.css" rel="stylesheet">
</head>

<body>
    <form class="form-signin">
        <h1 class="h3 mb-3 font-weight-normal text-center">Login</h1>
        <label for="inputEmail" class="sr-only">Email address</label>
        <input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" id="inputPassword" class="form-control" placeholder="Password" required>
        <div id="captcha" class="mb-3 text-center"></div>
        <button class="btn btn-lg btn-primary btn-block" disabled="" type="submit">Sign in</button>
    </form>
    <script src="catpcha.js"></script>
    <script type="text/javascript">
    const button = document.getElementsByTagName('button')[0];
    const captcha = document.getElementById('captcha');

    captcha.addEventListener('captcha.success', function() {
        button.disabled = false;
    });

    captcha.addEventListener('captcha.failed', function() {
        button.disabled = true;
    });
    </script>
</body>

</html>
Save all the files and when we refresh the page in the browser, we should see
You can find the above code here: arvindr21/captcha.

Thanks for reading! Do comment.
@arvindr21
The post Building a Captcha using HTML5 Canvas appeared first on The Jackal of Javascript.