Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Captcha Laravel Package

mobicms/captcha

Mobicms Captcha is a lightweight CAPTCHA package for Laravel that generates customizable image challenges to protect forms from bots. It offers a simple API for rendering and validating codes, with configurable options for easy integration into login, registration, and contact forms.

Deep Wiki
Context7

Getting Started

Install the package with Composer:

composer require mobicms/captcha

Because this is a framework-agnostic PHP package, Laravel will not auto-register a service provider. Start by reading the package source in vendor/mobicms/captcha/ and its README to confirm the exact class names and method signatures. The typical lifecycle is:

  1. Generate a challenge/code.
  2. Store the expected answer in the session.
  3. Output the CAPTCHA image or challenge markup.
  4. Compare the submitted value against the stored answer.
  5. Remove the stored answer after validation to prevent replay.

A minimal first use case is protecting a contact or login form:

// routes/web.php
Route::get('/captcha', [CaptchaController::class, 'image'])->name('captcha.image');
Route::post('/contact', [ContactController::class, 'store']);
// app/Http/Controllers/CaptchaController.php
public function image()
{
    // Inspect the package API for the exact generator call.
    // The goal is to obtain the generated code and the image output.
    $captcha = new \Mobicms\Captcha\Captcha();

    // Store the expected answer using Laravel's session, not $_SESSION.
    session(['captcha_answer' => $captcha->getCode()]);

    return response($captcha->getImage())
        ->header('Content-Type', 'image/png')
        ->header('Cache-Control', 'no-store, no-cache, must-revalidate');
}

In the form:

<form method="POST" action="/contact">
    @csrf

    <img src="{{ route('captcha.image') }}?t={{ now()->timestamp }}" alt="CAPTCHA">
    <input type="text" name="captcha" autocomplete="off">

    <button type="submit">Send</button>
</form>

Then validate on submission:

$request->validate([
    'captcha' => ['required', function ($attribute, $value, $fail) {
        $expected = session('captcha_answer');
        session()->forget('captcha_answer');

        if (! $expected || ! hash_equals(strtoupper($expected), strtoupper(trim($value)))) {
            $fail('The CAPTCHA answer is incorrect.');
        }
    }],
]);

The first thing to verify is whether the package writes directly to $_SESSION or exposes the generated code separately. If it tightly couples generation and validation through native PHP sessions, you will need to bridge it to Laravel’s session store or use only the image/code generation portion.

Implementation Patterns

Image route + Laravel session

The cleanest Laravel integration is to use the package only to generate the image and the expected code, then manage the answer yourself in Laravel’s session. This avoids conflicts between native PHP sessions and Laravel’s session driver.

Pattern:

  • GET /captcha generates a new code.
  • Store it under a namespaced session key such as captcha.answer.
  • Return the image with Content-Type: image/png and Cache-Control: no-store.
  • On form submission, compare and immediately forget the session key.

Custom validation rule

Encapsulate validation so it can be reused across forms:

class CaptchaRule implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $expected = session()->pull('captcha.answer');

        if (! $expected || ! hash_equals(strtoupper($expected), strtoupper(trim((string) $value)))) {
            $fail('The CAPTCHA answer is incorrect.');
        }
    }
}

Usage:

$request->validate([
    'captcha' => ['required', new CaptchaRule()],
]);

Refreshing the challenge

Add a cache-busting query string to the image URL so the browser requests a new challenge:

<img src="{{ route('captcha.image') }}?t={{ now()->timestamp }}" alt="CAPTCHA">

For an explicit refresh button, update the src with JavaScript:

<button type="button" onclick="document.getElementById('captcha-img').src = '{{ route('captcha.image') }}?t=' + Date.now()">
    Refresh
</button>

Each request to the image route overwrites the session answer. That is expected: only the most recently displayed CAPTCHA should validate.

Livewire / Inertia / SPA forms

The session-based flow still works as long as the CAPTCHA image route and the form submission both use Laravel’s web middleware group and therefore share the same session.

For Livewire, prevent the image from being re-rendered unnecessarily:

<div wire:ignore>
    <img id="captcha-img" src="{{ route('captcha.image') }}?t={{ now()->timestamp }}" alt="CAPTCHA">
</div>

For Inertia or SPA forms, render the <img> tag pointing to the Laravel route. The image request must include credentials/cookies so the session is preserved.

Centralized configuration

If the package allows configuration of length, fonts, colors, or image size, wrap instantiation in a small service or bind it in a service provider:

// app/Providers/AppServiceProvider.php
$this->app->bind(Captcha::class, function () {
    return new Captcha([
        'length' => 5,
        // other package-specific options
    ]);
});

Then inject Captcha into controllers instead of constructing it inline. This makes testing and future package replacement easier.

Testing

Do not generate real CAPTCHA images in most feature tests. Either:

  • Disable CAPTCHA via a config flag in the test environment, or
  • Seed the session with a known answer before submitting the form.

Example:

$this->withSession(['captcha.answer' => 'ABCDE'])
    ->post('/contact', [
        'captcha' => 'ABCDE',
        // other fields
    ]);

For unit tests around your validation rule, test the session comparison logic directly without invoking the image generator.

Rate limiting

CAPTCHA generation can be abused to consume CPU or fill sessions. Apply Laravel’s rate limiter to the image route:

Route::get('/captcha', [CaptchaController::class, 'image'])
    ->middleware('throttle:30,1')
    ->name('captcha.image');

Also consider throttling form submissions independently.

Gotchas and Tips

Native $_SESSION vs Laravel session

This is the most important integration risk. mobicms/captcha is a standalone PHP package and may use PHP’s native $_SESSION or expect you to manage session state yourself. Laravel uses its own session abstraction. Do not assume $_SESSION['captcha'] is the same as session('captcha').

Recommended approach: inspect the package API. If it exposes the generated code separately from the image, use that code and store it with Laravel’s session() helper. If it only validates through native session state, either:

  • Start and synchronize a native PHP session carefully, which is fragile under Laravel, or
  • Use the package only for image/code generation and write your own comparison logic.

Avoid mixing Laravel sessions and native PHP sessions in the same request unless you fully understand the interaction.

Session middleware must be present

The CAPTCHA image route and the form submission route must share the same session. In Laravel, that usually means both routes belong to the web middleware group. API routes often use the api group and do not have session support by default. A stateless API cannot use session-based CAPTCHA without adding cookie/session middleware or switching to a token-based approach.

Case sensitivity and whitespace

Generated CAPTCHA codes are often uppercase or mixed case. Normalize both sides before comparing:

hash_equals(
    strtoupper(trim($expected)),
    strtoupper(trim($submitted))
);

Use trim() to avoid failures caused by accidental spaces. Use hash_equals() for a timing-safe comparison.

Consume the answer after validation

Always remove the session answer after a validation attempt, even if it fails:

$expected = session()->pull('captcha.answer');

If you leave it in session, the same challenge can be reused for multiple submissions, weakening spam protection. If the validation fails, require the user to load a new CAPTCHA image.

Browser caching breaks refresh

If the CAPTCHA image is cached by the browser, users may see the same challenge even after a refresh. Always send no-cache headers and add a cache-busting query string:

return response($image)
    ->header('Content-Type', 'image/png')
    ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
    ->header('Pragma', 'no-cache');

Output buffering if the package echoes the image

Some lightweight CAPTCHA packages output the image directly instead of returning a string or resource. If so, capture it:

ob_start();
$captcha->render(); // or whatever the package method is
$image = ob_get_clean();

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

Check the package source for whether it calls header(), echo, or returns a value. If it sends headers directly, you may need to adapt or fork the rendering path.

GD extension requirement

Most image-based CAPTCHA packages require the PHP GD extension. If the image route returns a broken image or a 500 error, check that ext-gd is installed and enabled. For containerized or serverless deployments, this is a common missing dependency.

Accessibility

Image CAPTCHAs are inaccessible to screen-reader users. For production forms, consider offering an audio challenge or a simple non-image alternative, or use a different anti-spam mechanism such as honeypots or time-based checks. If accessibility is a hard requirement, this package may not be sufficient on its own.

Static state and Laravel Octane

Under Laravel Octane, long-lived workers can expose static state bugs. Verify that the package does not store per-request data in static properties or global variables. If it does, you may need to instantiate it per request and avoid caching the instance across requests.

Extension points

If the package supports custom fonts, colors, image dimensions, or code length, centralize those settings in a config file or service provider binding rather than scattering constructor arguments across controllers. This also makes it easier to swap the CAPTCHA implementation later.

Debugging checklist

If validation always fails:

  1. Confirm the image route and form route share the same session.
  2. Log the session key before and after the image request.
  3. Check whether the package uses native $_SESSION instead of Laravel’s session.
  4. Confirm the submitted field name matches the validation attribute.
  5. Check for whitespace, case differences, or accidental double submission.
  6. Verify the session answer is not being cleared by another request, such as a prefetch or refresh.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console