rahul900day/laravel-captcha
rahul900day/laravel-captcha adds easy CAPTCHA protection to Laravel forms, supporting reCAPTCHA, hCaptcha, and custom drivers through a clean API. It includes validation rules, Blade rendering, and configurable options so you can secure any form in minutes.
Install and publish the config
composer require rahul900day/laravel-captcha:^5.0
php artisan vendor:publish --tag=captcha-config
Version 5.0.0 is the Laravel 13 compatibility release — the only change in the notes is "Laravel 13 Support." Because it shipped as a major bump, assume the illuminate/* constraints in composer.json were widened upward and older framework versions dropped. Check that constraint before you commit: if you're still on Laravel 12 or earlier, stay on the 4.x line, and don't run a bare composer update on a project that mixes framework versions.
Laravel's package auto-discovery registers the service provider and the Captcha facade (Rahul900day\Captcha\Facades\Captcha). This is still a low-adoption package (0 dependents, ~108 stars), so version compatibility remains your first real risk — the 5.0.0 release proves it tracks Laravel majors, but it also proves a major of the framework forces a major of the package.
Set the driver in .env
CAPTCHA_DRIVER=math
# only needed for the remote drivers
RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
HCAPTCHA_SITE_KEY=
HCAPTCHA_SECRET_KEY=
If you change these after a deploy, run php artisan config:cache again or the old values stay baked in.
Where to look first
config/captcha.php — the default driver and the per-driver blocks. This tells you which drivers actually ship in your installed version.src/Drivers/ — each driver is a small class; read the one you plan to use to see exactly what create() returns and what validate() expects.Captcha facade docblock — the authoritative method signatures.First use case: a math captcha on a contact form
// ContactController@show
public function show()
{
return view('contact', [
'captcha' => Captcha::create('math'), // returns key + renderable payload
]);
}
{{-- contact.blade.php --}}
<img src="{{ $captcha['image'] }}" alt="Solve the challenge">
<input type="hidden" name="captcha_key" value="{{ $captcha['key'] }}">
<input type="text" name="captcha" required>
// ContactController@store
public function store(Request $request)
{
$request->validate(['captcha' => ['required', 'string']]);
if (! Captcha::validate($request->input('captcha_key'), $request->input('captcha'))) {
return back()
->withErrors(['captcha' => 'That answer was incorrect.'])
->withInput();
}
}
That create → render → validate loop is 90% of real-world usage. Everything below is variations on it.
Generate in the controller, not in Blade. Rendering Captcha::create() inside a view means it re-runs on every render of that view (including the back() redirect after a failed submit), producing a new key while the user's session still references the old one. Generate once per GET, pass the payload down.
Wrap the form half in a Blade component. It keeps the hidden-key/input pairing consistent and gives you one place to change driver-specific markup:
{{-- resources/views/components/captcha.blade.php --}}
@props(['captcha'])
<div {{ $attributes }}>
<img src="{{ $captcha['image'] }}" alt="CAPTCHA">
<input type="hidden" name="captcha_key" value="{{ $captcha['key'] }}">
<input type="text" name="captcha" required autocomplete="off">
</div>
Move validation into a FormRequest. The two-argument validate(key, value) call doesn't map cleanly onto a rule string, so do the check in withValidator() or after() so both your normal rules and the captcha failure surface in the same $errors bag.
Remote drivers (reCAPTCHA / hCaptcha) are a different shape. There's no server-generated key — you render the vendor widget with the site key, the browser posts a token, and you validate that token server-side using the secret key. Two consequences:
hostname and (for v3) action/score in the response, not just success. A token minted on someone else's site will pass success and fail everything else. If the driver doesn't expose those fields, call the vendor endpoint yourself.Inertia / Livewire / SPA front ends. Expose a tiny JSON endpoint:
Route::get('/captcha/{driver?}', fn (?string $driver = null) => response()->json(
Captcha::create($driver ?? config('captcha.default'))
))->middleware('throttle:30,1');
The client fetches a fresh challenge on mount and again on every validation failure. This is where the package earns its keep over hand-rolling — one endpoint covers math, image, and widget drivers behind the same contract.
Always pair it with rate limiting. Math and short-answer captchas have a tiny answer space. throttle:5,1 on the submit route is not optional; without it the captcha is decorative.
Testing. Feature tests will fail on captcha for the boring reason that the key store is array cache, which resets between requests. Two workable approaches:
phpunit.xml / phpunit.xml.dist env, so real requests skip the check.Captcha::shouldReceive('validate')->once()->andReturn(true); and keep one dedicated test asserting the failure path against the real driver.Swap-proof your code. The package is small and lightly adopted; the blast radius of it being abandoned is your whole auth/contact surface. Put a CaptchaService interface in front of it with generate() and verify() methods, and resolve the package facade inside that one class. When you need to move to a maintained alternative — or when the next Laravel major lands and 5.x hasn't caught up yet — it's a one-file change.
Upgrading to 5.0.0 is a framework-compat bump, not a feature bump. Nothing in the notes suggests API changes — just Laravel 13 support — but the major version means the old illuminate/* constraints are gone. Treat the upgrade as: bump the constraint, run the full suite against your captcha endpoints, and spot-check any custom driver you wrote, since the driver contract is the most likely thing to have moved. If you publish views or lang files, re-publish or diff them after upgrading in case the vendor paths shifted.
The key has a TTL and is single-use. If the user leaves the tab open for ten minutes, validation fails with a "wrong answer" message that looks like a user error but is really an expiry. Give the error copy room ("This challenge expired — please try again") and always regenerate on failure. Never re-render the same key back into the form.
Cache/session configuration matters. Depending on the version, generated keys live in the cache or session store. CACHE_DRIVER=array (Laravel's test and some local defaults) silently discards them between requests, which produces "captcha always fails locally but works in staging." Check config/cache.php before you file a bug against the package.
Image driver needs GD and a font. extension_loaded('gd') returning false is the most common cause of a broken image. The payload is a base64 data URI, so there's no storage disk involved — but it does mean a fat HTML response per form; don't put three image captchas on one page.
Config cache is a trap during rollout. CAPTCHA_DRIVER changes without php artisan config:cache will appear to do nothing. Same for secret keys — a stale cached config with an empty secret produces vendor API errors that read like network failures. This bites hardest right after a major upgrade, when you're changing both the package version and the config in the same deploy.
Remote driver debugging. Log the raw vendor response before you interpret it. A success: false with error-codes: ["invalid-input-response"] means expired/duplicate token; ["timeout-or-duplicate"] means you validated twice. Almost every "reCAPTCHA is broken" report is one of those two, or a domain that isn't whitelisted for the site key in the vendor console.
Never log or expose the secret key. The site key is public by design; the secret key is not. Keep both in .env, never in config/captcha.php literals, or config:cache will ship them into a file that gets committed by accident.
Accessibility is on you. Image and math captchas have no audio fallback in a package this size. If your form must be WCAG-compliant, plan for either the remote drivers (which ship audio challenges) or a honeypot-plus-timing approach for low-risk forms.
Extension points worth knowing. A custom driver is usually a small class implementing the package's driver contract plus a config block — cheaper than forking. Language strings and views (where published) live under resources/lang/vendor/ and resources/views/vendor/, so error copy can be localized without touching the source.
Treat the dependency as fragile. MIT license, no dependents, and a single maintainer. The 5.0.0 release is reassuring — the package is alive and tracks Laravel majors — but it also means your upgrade path is coupled to the maintainer's free time, and there's no backport promise for the version you're on. Pin the exact or caret version in composer.json, watch the repo whenever a new Laravel major ships, and keep the abstraction layer described above. The package is a reasonable way to get three captcha types behind one API quickly; it is not something to build a compliance story on without a fallback.
How can I help you explore Laravel packages today?