Reader Stacks

How to Add a Custom CAPTCHA to a Laravel Form

A simple session-stored, image-rendered CAPTCHA — generated with PHP's GD library, validated against the session, and refreshable without a full page reload.

How to Add a Custom CAPTCHA to a Laravel Form

A basic, self-hosted CAPTCHA generates a random string, renders it as a distorted image using PHP's GD library, stores the expected answer in the session, and validates the user's typed answer against it — no third-party service required, though a service like reCAPTCHA (covered separately elsewhere on this site) is generally the more robust choice against real automated bots.

Generating the CAPTCHA image

Route::get('/captcha', function () {
    $code = strtoupper(substr(str_shuffle('ABCDEFGHJKLMNPQRSTUVWXYZ23456789'), 0, 6));
    session(['captcha_code' => $code]);

    $image = imagecreate(150, 50);
    imagecolorallocate($image, 255, 255, 255);
    $textColor = imagecolorallocate($image, 0, 0, 0);

    imagestring($image, 5, 30, 15, $code, $textColor);

    // add some noise lines to make it harder for basic OCR to read
    for ($i = 0; $i < 5; $i++) {
        imageline($image, rand(0, 150), rand(0, 50), rand(0, 150), rand(0, 50), imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200)));
    }

    ob_start();
    imagepng($image);
    $imageData = ob_get_clean();
    imagedestroy($image);

    return response($imageData)->header('Content-Type', 'image/png');
});

Excluding easily-confused characters (like 0/O and 1/I) from the random character pool, as the example above does, reduces genuine user frustration from a CAPTCHA that's ambiguous even to a real person.

The form with the CAPTCHA image and input

@csrf CAPTCHA Refresh

Refreshing the CAPTCHA without a full page reload

function refreshCaptcha() {
    document.getElementById('captcha-image').src = '/captcha?t=' + Date.now();
}

Appending a changing query parameter (like the current timestamp) is necessary specifically to defeat the browser's own image caching — without it, the browser may simply reuse the previously cached image instead of actually requesting a fresh one from the server.

Validating the answer on form submission

public function store(Request $request)
{
    $request->validate([
        'captcha_answer' => 'required',
    ]);

    if (strtoupper($request->captcha_answer) !== session('captcha_code')) {
        return back()->withErrors(['captcha_answer' => 'Incorrect CAPTCHA code.']);
    }

    session()->forget('captcha_code');

    // proceed with the actual form processing
}

Clearing captcha_code from the session after a successful check prevents the same code from being reused for a second submission attempt.

Why a service-based CAPTCHA (like reCAPTCHA) is generally the more robust choice

A self-hosted image CAPTCHA like this one is genuinely defeatable by modern OCR and automated solving services at any real scale — it's a reasonable, dependency-free option for a low-stakes form, but for anything facing meaningful spam or bot traffic, Google reCAPTCHA's behavioral analysis (covered elsewhere on this site) offers considerably stronger protection.