shetabit/captcha
A Laravel CAPTCHA package from Shetabit for adding and validating CAPTCHA challenges in forms, helping protect routes and submissions from automated abuse with configurable settings and simple Blade integration.
Install and publish config
composer require shetabit/captcha
php artisan vendor:publish --provider="Shetabit\Captcha\CaptchaServiceProvider" --tag="config"
Laravel package discovery registers the service provider automatically; the package also registers its image routes for you. After publishing you should have config/captcha.php with a default entry (length, width, height, quality, math, expire, fonts, background settings).
Where to look first
config/captcha.php — every knob you'll actually touch lives here, including named presets.captcha_src(), captcha_img(), captcha_check().Shetabit\Captcha\Rules\CaptchaRule — the validation rule behind the captcha alias.Captcha facade, if you need programmatic generation or route registration.vendor/shetabit/captcha/routes/ and the service provider — confirm which endpoints v2.1.0 exposes on your install (image route plus a JSON/API route) and which middleware group they're attached to.First use case — protect a login or registration form:
{{-- resources/views/auth/login.blade.php --}}
<form method="POST" action="{{ route('login') }}">
@csrf
...
<img id="captcha" src="{{ captcha_src() }}" alt="captcha">
<button type="button" onclick="document.getElementById('captcha').src='{{ captcha_src() }}?'+Date.now()">
Refresh
</button>
<input type="text" name="captcha" required>
@error('captcha') <span>{{ $message }}</span> @enderror
</form>
// app/Http/Requests/LoginRequest.php
public function rules(): array
{
return [
'email' => ['required', 'email'],
'password' => ['required'],
'captcha' => ['required', 'captcha'],
];
}
That's the whole happy path: generate on render (the session stores the expected answer), validate on submit (the rule consumes and forgets it).
Named presets for different surfaces. Define more than one block in config/captcha.php (default, math, mini, flat, …) and pass the key: captcha_src('math') renders the math variant. Keep one preset per form type — a numeric-math captcha for a newsletter box and a string captcha for signup — rather than one global setting.
FormRequest vs. inline rule. The captcha alias is fine for typical forms. When you need a specific preset or explicit control, instantiate the rule object:
use Shetabit\Captcha\Rules\CaptchaRule;
$request->validate([
'captcha' => ['required', new CaptchaRule()],
]);
Manual checks outside FormRequests. For controller-level or API validation, captcha_check($value) reads the session entry directly. Guard it with a required check first — an empty string is not the same failure mode.
SPA / Inertia / Vue. The image endpoint is session-backed, so the browser must send cookies. With Sanctum's SPA auth (same-origin, withCredentials/axios.defaults.withCredentials = true) a plain <img src="/captcha/default"> works, and you submit the typed answer with the rest of the payload. If you prefer a JSON payload, use the package's API endpoint (returns the image inline), which is the cleaner fit for mobile clients that have no cookie jar — but then you own the session/state correlation, so verify the route exists and is in the web middleware group before designing around it.
Livewire. Don't hold the image URL in a public property that never changes — the browser caches it. Render captcha_src() with a per-render cache-buster, and refresh the image after a failed validation (see gotchas).
Testing. Captcha in tests is friction, not coverage. Prefer disabling the rule for the test suite rather than trying to reverse-engineer the session payload:
// tests/TestCase.php or a dedicated trait
protected function setUp(): void
{
parent::setUp();
if (app()->environment('testing')) {
// swap the rule alias, or conditionally drop 'captcha' from rules
}
}
A Captcha::fake()-style helper isn't part of the package's contract; keep the bypass in your own test trait (or a config-driven enabled flag you check in a custom rule that extends CaptchaRule) so a package upgrade can't silently break it.
Error message localization. The rule's message comes through the validator; put a translation in resources/lang/{locale}/validation.php under the captcha key (or publish the package's lang files if it ships them) rather than hardcoding English in a custom message argument.
GD with FreeType is mandatory. Image generation needs the gd extension compiled --with-freetype. A missing FreeType produces a broken image or a 500 depending on PHP version — check php -m | grep -i gd and gd_info()['FreeType Support'] before you debug your Blade template.
The captcha is one-time-use. A successful validation forgets the session value. After a failed validation elsewhere on the form, the old image is already consumed-in-waiting — always refresh the image when redisplaying a form with errors, otherwise users get a second, confusing "invalid captcha" failure.
Browser caching. The image URL is static per preset. Without a ?t= timestamp on the refresh button, "click to refresh" appears to do nothing. This is the single most-reported confusion with this package.
Two captchas on one page will collide. Presets share a single session slot; a login modal and a register modal both rendering a captcha will overwrite each other. Either keep one captcha per page, or use distinct presets and verify the validation rule actually keys off the preset in your installed version — if it only reads the last-generated value, split the forms.
Don't put the API endpoint behind api middleware. api has no session; a session-backed captcha there will always fail validation. Confirm the route's middleware in the service provider and, if needed, re-register it yourself with web.
Expiry is a real setting. expire (minutes, default ~60) is checked at validation time. Long-lived forms — a multi-step checkout — will fail captcha for slow users. Raise it deliberately or regenerate per step.
Case sensitivity and math mode. Math presets expect the numeric answer ("7", not "7 " — trim input); string presets may be case-sensitive depending on config. Sanitize before validating rather than letting users discover this.
Route caching is fine, config caching isn't automatic. After editing config/captcha.php on a cached deployment, run php artisan config:clear / re-cache — otherwise you'll swear the font path change had no effect.
Font and background paths are filesystem paths. Use base_path() / public_path() in config, not URLs, and make sure the files ship in your deploy artifact (they're often missed by .gitignore-happy CI pipelines).
Image generation is CPU-bound. Each hit rasterizes a PNG. Put the captcha route behind a modest rate limit or a short-lived cache header, especially on registration endpoints that are already bot targets — otherwise the captcha becomes the DoS vector.
Verify the changelog before upgrading. v2.1.0's release note is only a link to the full diff (v2.0...v2.1.0) with no summary. Before bumping in production, read CHANGELOG.md/the compare view for changes to the config schema, route middleware, or session key — those are the three things that break an existing integration silently rather than with an exception.
How can I help you explore Laravel packages today?