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

jason/captcha

jason/captcha is a lightweight CAPTCHA package for Laravel that helps protect forms from automated submissions with simple challenge generation, validation, and configurable routes, views, and styling options.

View on GitHub
Deep Wiki
Context7

Getting Started

Treat this as a read-the-source-first package. With 6 stars and zero dependents there is no blog-post trail, no Stack Overflow corpus, and no third-party tutorials to fall back on — the repository is the documentation.

Install and confirm compatibility before anything else:

composer require jason/captcha

Then open composer.json inside the package and check the require block for the Laravel/Illuminate version constraint and the PHP constraint. A package with this little traffic is often pinned to a narrow framework range; if it declares illuminate/support: ^9.0 and you are on Laravel 11, you may need --ignore-platform-reqs (and you should then expect breakage). Confirm the PHP extensions it needs — image captchas almost always require gd or imagick:

php -m | grep -iE 'gd|imagick'

Where to look first, in this order:

  1. src/ — find the service provider (typically src/CaptchaServiceProvider.php or src/LaravelCaptchaServiceProvider.php). Read register() and boot(): this tells you the container binding name, whether a facade alias is registered, whether routes/views/config are published, and whether a validation rule is extended.
  2. config/ — the publishable config file gives you the real option surface (length, char set, image dimensions, fonts, expiry, case sensitivity).
  3. routes/ — if the provider calls loadRoutesFrom(), the package ships its own endpoints (usually a single image-serving GET route). That route is your integration point.
  4. resources/views/ and src/Http/Controllers/ — the controller shows exactly how the code is stored (session key? cache key? encrypted token?) and how the image is rendered.
  5. README.md — read it last, not first. At this maturity level it is often a stub or out of sync with the code.

Publish what is publishable:

php artisan vendor:publish --provider="Jason\Captcha\CaptchaServiceProvider"

Run it with no --tag first so you see every available tag, then publish deliberately.

First use case — a login or registration form. The canonical flow for this class of package is: the form view renders the captcha (either an <img> pointing at the package route, or an inline base64 data URI), the correct answer is stashed in the session under a package-defined key, and on POST you validate the submitted value against that key and then invalidate it. Get that single flow working end to end before touching configuration.

Implementation Patterns

Session-backed form flow. The dominant pattern. Render in Blade and validate in the FormRequest or controller:

// in the form
{!! captcha_img() !!}       // or <img src="{{ route('captcha') }}">
<input type="text" name="captcha">

// in the FormRequest rules
'captcha' => ['required', 'captcha'],   // if the package registers a rule

If the package registers a custom validation rule (check Validator::extend in the provider), use it — it will handle case-folding and one-time invalidation for you. If it does not, write the comparison yourself but replicate the invalidation.

Wrap it in your own rule or helper regardless. Do not scatter session()->get('captcha') comparisons across controllers. Create app/Rules/Captcha.php that delegates to the package and always forgets the session key afterwards. This is your insulation layer when you eventually swap the package out — a realistic outcome here.

Stateless / API usage. If the package supports returning the image as a base64 string or a token instead of binding to a PHP session, prefer that for SPA and mobile clients. Pattern: GET /api/captcha returns { id, image }, store the answer server-side keyed by id in the cache with a short TTL, and POST includes both id and answer. Verify the ID is single-use — delete on read, not on success.

Protect the captcha endpoint itself. The image route is public and does real CPU work (font rendering, noise/line generation). Put it behind throttle: middleware:

Route::middleware('throttle:30,1')->get('/captcha', ...);

Without this, an unauthenticated attacker can trivially exhaust CPU. This is the single most common production mistake with self-hosted captcha packages.

Refresh/regeneration UX. Expose a "click to refresh" affordance that re-requests the endpoint with a cache-busting query string (?t= + timestamp) and, critically, invalidates the previous answer server-side. Otherwise you leave a trail of valid answers in the session.

Layering with other defences. A 4-character image captcha is not a bot defence on its own. Pair it with throttle on the login route, and if you are under real attack, put Cloudflare Turnstile or a proof-of-work challenge in front. The captcha is a speed bump for naive scripts; treat it as such.

Testing. Find the bypass first. Many small packages check app()->environment('testing') or a config flag and accept any input. If not, set the session value directly in your test:

$this->withSession(['captcha' => 'abcde'])->post('/login', [...'captcha' => 'abcde']);

Write one test for the happy path and one asserting that a wrong answer is rejected — you are defending against a future regression in a package you do not control.

Gotchas and Tips

Session driver. If the answer lives in the session and your test suite or queue worker uses the array driver, validation will silently fail across requests. Use database or redis in test config, or bypass explicitly.

Session vs. cache TTL drift. The answer's lifetime is governed by session GC and any package-level expiry config. A user who leaves a login tab open for 40 minutes gets a confusing "invalid captcha" with no visual indication the image expired. Either make the TTL short and obvious, or check on validation whether the key still exists and return a distinct "expired, please refresh" message.

One-time use must be enforced. Forget the answer on every outcome — correct or incorrect — or a single solved captcha becomes a replay token for the rest of the session.

Browser and proxy caching of the image. Image-generating controllers frequently omit Cache-Control: no-store, no-cache. Without it, a CDN or the browser back-button cache can serve a stale image whose answer no longer matches. Set the headers in your own wrapper route if the package doesn't.

Font and asset paths break under php artisan config:cache and in containers. Image captchas resolve font files via __DIR__ or public_path(). After config:cache, or when deployed via a read-only container image, a relative path that worked locally can fail. Check the config value points at an absolute, published path, and that the file ships in your Docker image.

GD is not optional and not always present. Alpine-based PHP images need docker-php-ext-install gd with --with-freetype. A missing freetype means imagettftext fails while the endpoint still returns a 200 with a broken image — a failure mode that looks like "the captcha never works."

Configuration is read at boot. If you change captcha length or character set in .env and see no effect, you almost certainly forgot php artisan config:cache (or need to clear it). Never call config() at runtime inside the image generator expecting fresh env values.

Character set ambiguity. If 0/O, 1/l/I, 5/S are all in the pool, users will fail legitimately and blame your app. Check the config's char set for confusables and, if you can't change it, lower the case-sensitivity flag.

Accessibility. A visual-only captcha blocks screen-reader users and is an ADA/EAA exposure. Look for an audio alternative in the package; if there isn't one, provide a documented support path or an email-verification fallback rather than shipping the wall as-is.

Bus factor is the real gotcha. Six stars, no dependents, and a single maintainer means you should not let this package's API leak into your domain code. Pin an exact version in composer.json ("jason/captcha": "1.2.3", not ^1.0), keep the integration behind one class as described above, and accept that you are likely to fork or replace it. When you do, the realistic alternatives are mews/captcha (session-based, actively used), gregwar/captcha (framework-agnostic, wraps easily), or a hosted challenge like Turnstile — which also removes the GD dependency, the CPU cost, and the accessibility problem in one move.

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