Complete Guide to Building CAPTCHA for Web Security with Node.js
Introduction
In today's digital age, securing web applications is more important than ever. Implementing CAPTCHA is a simple yet effective way to prevent automated abuse. This blog post will guide you through creating a CAPTCHA generation tool using Node.js and the Canvas library, demonstrating how easy it is to add this security feature to your web applications.
Initial Setup
Prerequisites
First, ensure that you have Node.js installed on your system. This guide uses Node.js version 20.11.1, but it's always best to use the latest version available.
Project Initialization
Start by creating a new project folder. You can name it anything you like, but for this tutorial, we'll call it easy-captcha
. You can create this folder manually or use the terminal with the following commands:
mkdir easy-captcha
cd easy-captcha
Setting Up the Project
To manage project dependencies, initialize a package.json
file using:
npm init -y
Next, install TypeScript as a development dependency and create a tsconfig.json
file:
npm install --save-dev typescript
npx tsc --init
In your tsconfig.json
, update the configuration to set the output directory:
"outDir": "./dist"
Package Installation
Install the necessary packages with the following commands:
For dependencies:
npm install canvas express http-status-codes
For development dependencies:
npm install --save-dev @types/express @types/node ts-node
Here are the versions of the packages used in this project, though you can use the latest versions available:
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.9",
"ts-node": "^10.9.2",
"typescript": "^5.5.2"
},
"dependencies": {
"canvas": "^2.11.2",
"express": "^4.19.2",
"http-status-codes": "^2.3.0"
}
Project Structure
Organize your project as follows:
📦app
┣ 📂src
┃ ┣ 📂controllers
┃ ┃ ┗ 📜index.ts
┃ ┣ 📂middleware
┃ ┃ ┗ 📜index.ts
┃ ┣ 📂routes
┃ ┃ ┗ 📜index.ts
┃ ┗ 📂utils
┃ ┃ ┣ 📜canvas.generate.utils.ts
┃ ┃ ┣ 📜index.ts
┃ ┃ ┣ 📜randomText.generate.ts
┃ ┃ ┗ 📜response.utils.ts
┗ 📜index.ts
Development
We'll start by creating some utility functions. Here's a brief overview of what each file will contain:
canvas.generate.utils.ts
: Functions for generating CAPTCHA images using Canvas.randomText.generate.ts
: Functions for generating random text for CAPTCHA.response.utils.ts
: Utility functions for sending responses.index.ts
: Entry point of the application.controllers/index.ts
,middleware/index.ts
, androutes/index.ts
: Placeholder files for organizing controllers, middleware, and routes.
Response Utility
The response.utils.ts
file contains a utility function for sending standardized JSON responses in an Express.js application. This utility ensures that your responses are consistent and properly formatted, making it easier to handle client-server communication.
import { getReasonPhrase } from "http-status-codes";
import { Response } from "express";
const response = (
res: Response,
code: number,
status: boolean,
data: object | null,
mesage: string
) => {
if (!mesage) mesage = getReasonPhrase(code);
return res.status(code).json({
status,
data,
mesage,
});
};
export default response;
Parameters
res
(Response): The Express.js response object is used to send the HTTP response.code
(number): The HTTP status code for the response (e.g., 200 for OK, 404 for Not Found).status
(boolean): A boolean value indicating the success (true) or failure (false) of the request.data
(object | null): The data to be included in the response body. This can be any JavaScript object or null if no data is sent.mesage
(string): A custom message for the response. If no message is provided, it defaults to the reason phrase for the given status code.
Random Text Generation Utility
The randomText.generate.ts
file contains a function to generate random text strings based on specified criteria. This utility can be used for creating random strings for CAPTCHAs or other purposes.
export const generateRandomText = (
allowNumber: boolean,
length: number,
uppercase: boolean
) => {
let characters = "abcdefghijklmnopqrstuvwxyz";
if (allowNumber) characters += "0123456789";
if (uppercase) characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
Parameters
allowNumber
(boolean): If true, the generated string can include numbers.length
(number): The desired length of the generated string.uppercase
(boolean): If true, the generated string can include uppercase letters.
The function starts with a base set of lowercase alphabetic characters: "abcdefghijklmnopqrstuvwxyz"
. If allowNumber
is true, it appends numeric characters ("0123456789"
) to the character set. If uppercase
is true, it appends uppercase alphabetic characters ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
) to the character set.
CAPTCHA Generation Utility
The canvas.generate.utils.ts
file contains functions for generating CAPTCHA images using the Canvas library.
const { createCanvas, registerFont } = require("canvas");
const generateCoreCaptcha = (text: string) => {
const canvas = createCanvas(200, 100);
const ctx = canvas.getContext("2d");
// Set background color
ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Set text properties
ctx.font = "30px Arial";
ctx.fillStyle = "#000";
// Rotate and draw each character individually
const xStart = 20;
const yStart = 50;
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
ctx.save();
const x = xStart + i * 30;
const y = yStart + (Math.random() * 10 - 5);
ctx.translate(x, y);
ctx.rotate(Math.random() * 0.4 - 0.2); // Rotate between -0.2 and 0.2 radians
ctx.fillText(char, 0, 0);
ctx.restore();
}
// Add noise lines
for (let i = 0; i < 8; i++) {
ctx.strokeStyle = "#" + ((Math.random() * 0xffffff) << 0).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();
}
// Add random dots
for (let i = 0; i < 100; i++) {
ctx.fillStyle = "#" + ((Math.random() * 0xffffff) << 0).toString(16);
ctx.beginPath();
ctx.arc(
Math.random() * canvas.width,
Math.random() * canvas.height,
1,
0,
Math.PI * 2
);
ctx.fill();
}
return canvas;
};
export const generateCaptcha = (text: string, convert?: string) => {
if (convert === "base64") return generateCoreCaptcha(text).toDataURL();
return generateCoreCaptcha(text);
};
The index.ts
file in the utils
directory serves as a central export point for all utility functions within the directory. This approach simplifies the import statements in other parts of the application by consolidating all utility exports into a single file.
export * from "./response.utils";
export * from "./canvas.generate.utils";
export * from "./randomText.generate";
Now let's write controller, middleware, and route one after another.
Create controllers
under src
folder. Let's create index.ts
under controllers
. The code block will be
import { Request, RequestHandler, Response } from "express";
import { StatusCodes } from "http-status-codes";
import response from "../utils/response.utils";
import { generateCaptcha, generateRandomText } from "../utils";
const homecontroller: RequestHandler = (req: Request, res: Response): void => {
const { text, type }: { text?: string; type?: string } = req.query;
if (text && text.length > 5) {
return response(
res,
StatusCodes.BAD_REQUEST,
false,
{
captcha: text,
image: null,
},
"Text is tool long !"
);
}
const randomText = text ?? generateRandomText(true, 5, true);
const canvas = generateCaptcha(randomText);
if (type === "base64")
return response(
res,
StatusCodes.ACCEPTED,
true,
{
captcha: randomText,
image: generateCaptcha(randomText, "base64"),
},
"Success !"
);
else {
res.setHeader("Content-Type", "image/png");
return canvas.createPNGStream().pipe(res);
}
};
export default homecontroller;
On homecontroller
first of all, check the text length if the text is available otherwise return a BAD_REQUEST
. if there is given text as req.query
then the randomText
will be that otherwise, it will generate randomText
using the generateRandomTextutils
. if the type
is base64
then it will return a response as JSON
; otherwise, it will return as a stream after generating a captcha using generateCaptcha
.
Create middleware
under src
folder. Let's create index.ts
under middleware
. The code block will be
import { Request, Response, NextFunction } from "express";
export const notFoundHandler = (
req: Request,
res: Response,
next: NextFunction
): any => {
return res.status(404).json({
status: false,
data: null,
message: "Not Found",
});
};
The piece of code will be used to if there is no page available for the link.
Create routes
under src
folder. Let's create index.ts
under routes
. The code block will be
import { Router } from "express";
import homecontroller from "../controllers/";
const router: Router = Router();
router.get("/", homecontroller);
export default router;
Finally crate index.ts
on src
.
import express, { Application } from "express";
import { Server } from "http";
import home from "./src/routes";
import { notFoundHandler } from "./src/middleware";
const app: Application = express();
app.use("/", home);
app.use(notFoundHandler);
const port: number = Number(process.env.PORT || 3000);
const server: Server = app.listen(port, () => console.log(`🚀 on ${port}`));
Import all the routes
and middleware
and initialize express.js
for the server.
To run the project we need to change something on package.json
. In the scripts section of the package.json
update some code.
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "node ./dist/index.js",
"dev": "ts-node app/index"
},
To run the project locally use
npm run dev
To build the project use
npm run build
API Reference
Index
By hitting the root endpoint (/
), the API returns a PNG image containing a captcha.
GET /
Example image:
Custom Text
You can provide your text using a query parameter.
GET /?text=<YOUR_TEXT>
Example image:
For example, using c5Uop:
GET /?text=c5Uop
Note: The text cannot be more than 5 characters long. If it exceeds 5 characters, an error response is returned:
{
"status": false,
"data": {
"captcha": "c5Uop55",
"image": null
},
"message": "Text is too long!"
}
Base64 Response
You can get a JSON response with the text and image base64 string using a query parameter:
GET /?type=base64
Example image:
Conclusion
This tutorial walks you through building a CAPTCHA generation tool with Node.js and Canvas, enhancing your web application's security effortlessly. You'll set up a clear project structure, generate CAPTCHA images dynamically, and handle responses effectively. By implementing these steps, you'll bolster your site's defenses against automated attacks, ensuring a safer experience for your users.
Subscribe to my newsletter
Read articles from M. H. Nahib directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
M. H. Nahib
M. H. Nahib
Hi, I am Nahib. Working as a software engineer at ImpleVista.