Building a Simple CAPTCHA System in JavaScript Without Dependencies

Building a Simple CAPTCHA System in JavaScript Without Dependencies

Building a Simple CAPTCHA System in JavaScript Without Dependencies

 

Spam and automated bots are common nuisances when building web forms. To prevent bot submissions, a well-known solution is the CAPTCHA—Completely Automated Public Turing test to tell Computers and Humans Apart. While there are third-party services like Google reCAPTCHA, there are situations where integrating an external service isn’t ideal. In this article, we’ll learn how to build a basic CAPTCHA system in pure JavaScript using the HTML5 Canvas API—no external libraries required.

1. Basics of CAPTCHA and Canvas Integration

CAPTCHAs typically involve generating and displaying a distorted set of characters that a human can recognize but a bot cannot easily parse. To do this ourselves, we can use the Canvas API to draw randomized characters and then validate that user input matches.

Let’s start by setting up the basic HTML structure.

<form id="captcha-form" onsubmit="return validateCaptcha()">
  <canvas id="captcha-canvas" width="150" height="50"></canvas>
  <br>
  <input type="text" id="captcha-input" placeholder="Enter CAPTCHA" />
  <button type="button" onclick="generateCaptcha()">Refresh CAPTCHA</button>
  <button type="submit">Submit</button>
</form>

This gives us a canvas to draw our CAPTCHA, an input field, and buttons to refresh and submit.

2. Generating Random CAPTCHA Text

We’ll start by creating a JavaScript function that generates a random string for our CAPTCHA. This string will then be drawn on the canvas.

let captchaText = "";

function generateRandomText(length = 6) {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
  let text = '';
  for (let i = 0; i < length; i++) {
    text += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return text;
}

Note: We purposely exclude ambiguous characters like ‘O’, ‘0’, ‘I’, and ‘l’ for better user experience.

Now we’ll add a function to draw the CAPTCHA using canvas.

function generateCaptcha() {
  captchaText = generateRandomText();
  const canvas = document.getElementById("captcha-canvas");
  const ctx = canvas.getContext("2d");

  // Clear previous drawing
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Background
  ctx.fillStyle = '#f2f2f2';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Add random lines for obfuscation
  for (let i = 0; i < 5; i++) {
    ctx.strokeStyle = '#'+Math.floor(Math.random()*16777215).toString(16);
    ctx.beginPath();
    ctx.moveTo(Math.random()*canvas.width, Math.random()*canvas.height);
    ctx.lineTo(Math.random()*canvas.width, Math.random()*canvas.height);
    ctx.stroke();
  }

  // Draw CAPTCHA text
  ctx.font = '24px Arial';
  ctx.fillStyle = '#333';
  const spacing = canvas.width / (captchaText.length + 1);

  for (let i = 0; i < captchaText.length; i++) {
    const char = captchaText.charAt(i);
    const x = spacing * (i+1);
    const y = 30 + Math.random() * 10; // Slight vertical jitter
    const angle = Math.random() * 0.4 - 0.2; // Rotate slightly
      
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(angle);
    ctx.fillText(char, 0, 0);
    ctx.restore();
  }
}

Here, we insert randomness in position and rotation to distort the text slightly, helping prevent OCR by bots.

3. Verifying CAPTCHA Submission

At this point, users can visually see a CAPTCHA. Let’s now validate the user input against the generated string.

function validateCaptcha() {
  const userInput = document.getElementById('captcha-input').value;
  if (userInput === captchaText) {
    alert("CAPTCHA verified successfully!");
    return true;
  } else {
    alert("CAPTCHA does not match, please try again.");
    generateCaptcha(); // Optionally refresh CAPTCHA on error
    return false;
  }
}

This client-side validation ensures the user input matches the current CAPTCHA. Although it deters basic bots, it’s important to understand this method is not foolproof in high-stakes environments, as it can be reverse engineered on the client side.

4. Improving Security and User Experience

To further enhance the CAPTCHA system:

  • Limit Attempts: Track failed attempts locally and block after a threshold.
  • Session Storage: Use a backend to serve the CAPTCHA and store the text server-side.
  • Accessibility: As CAPTCHAs can become barriers to users with disabilities, consider adding an audio version or alternative.
  • Random Noise: Add dots or arcs over the text to make it more complex.

Example of adding noise:

for (let i = 0; i < 30; i++) {
  ctx.beginPath();
  ctx.arc(Math.random()*canvas.width, Math.random()*canvas.height, 1, 0, 2*Math.PI);
  ctx.fillStyle = '#'+Math.floor(Math.random()*16777215).toString(16);
  ctx.fill();
}

This makes the CAPTCHA harder to read programmatically but still reasonable for humans.

5. Tips and Performance Considerations

While the CAPTCHA here is lightweight and practical for hobby projects, keep these considerations in mind:

  • Canvas Performance: Avoid dynamically re-generating the canvas on every keystroke. Limit it to initial render or upon user request.
  • Reproducibility: Once a CAPTCHA is generated, avoid regenerating it on actions like form field changes.
  • Form Events: Use window.onload or DOMContentLoaded to ensure CAPTCHA renders on page load.

Example:

window.onload = function() {
  generateCaptcha();
};

For production-grade websites, we recommend server-side or hybrid CAPTCHA implementations. However, for static sites or client-heavy apps, this method can add a helpful security layer with minimal footprint.

Conclusion

We’ve seen how to build a simple, dependency-free CAPTCHA system using JavaScript and HTML5 Canvas. This custom solution provides a lightweight way to deter automated form submissions for straightforward use cases. With care for security around exposure and user experience, such a CAPTCHA can be a valuable addition to your frontend toolbox.

Happy Coding!

 

Useful links: