grantholle/laravel-altcha
Laravel ALTCHA offers a privacy-friendly, proof-of-work CAPTCHA alternative for Laravel apps. Add the Blade widget, verify submissions server-side, and configure challenges without third-party tracking or frustrating image puzzles.
grantholle/laravel-altcha wraps the altcha-org/altcha PHP library so you can plug a privacy-friendly, proof-of-work CAPTCHA into a Laravel app in a couple of lines. The flow is: the browser solves a challenge (via the <altcha-widget> web component), submits a base64 payload back to your server, and Laravel verifies it against a signed HMAC.
Minimal setup:
composer require grantholle/laravel-altcha
Publish the config: php artisan vendor:publish --tag=altcha-config
Generate an HMAC secret and put it in .env:
ALTCHA_HMAC_KEY=base64:some-long-random-string
Add the widget to your form (Blade shown, same idea in Livewire/Inertia):
<script src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js" type="module" defer></script>
<altcha-widget challengeurl="{{ route('altcha.challenge') }}"></altcha-widget>
Validate on the server:
use Grantholle\LaravelAltcha\Rules\Altcha;
$request->validate([
'altcha' => ['required', new Altcha()],
]);
The first place to look is config/altcha.php — it controls the HMAC key, algorithm, cost (work factor), expiry window, and (as of 2.3.0) the new single-use toggle. Then skim src/Rules/Altcha.php — it's tiny and tells you exactly what the rule asserts.
The simplest first use case is protecting a public form (contact, registration, password reset) where you want bot friction without a third-party service or cookies.
FormRequest-based validation. Wrap the rule in a FormRequest so it's testable and consistent:
class ContactRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email'],
'message' => ['required', 'string', 'max:5000'],
'altcha' => ['required', new Altcha()],
];
}
}
Route registration. The package registers a challenge route (typically GET /altcha/challenge) out of the box. If you need to customize path, middleware, rate limiting, or throttling, publish/override the config and re-register the route yourself with Route::get(...) pointing at the package controller.
API/SPA flows. For JSON APIs, include the altcha payload in the request body and return a structured 422 with errors.altcha — the rule plays nicely with Laravel's validation response since it's a standard rule.
Single-use solutions (new in 2.3.0). The release adds opt-in single-use solutions on top of altcha-org/altcha v2. Enable it when replay of a solved challenge is a real risk (login, password reset, payment initiation). The pattern is:
config/altcha.php.Rate limiting the challenge endpoint. The challenge endpoint is cheap to serve but should still be throttled (throttle:60,1) — otherwise bots can farm challenges to burn CPU elsewhere. Do this via route middleware or a wrapper route.
Testing. In feature tests, the easiest path is binding a fake rule that always passes, or generating a valid solution in-test against the configured HMAC key. Do not disable the rule globally — assert that the rule fails for missing/tampered payloads in at least one test.
Inertia/Inertia + Vue/React. Mount the <altcha-widget> the same way; the payload comes back as a hidden input named altcha and flows through your useForm submission as-is.
ALTCHA_HMAC_KEY while a form is open, that user's solve is rejected. Either rotate in quiet periods or support two keys during a transition window (you'll need to hook into the rule).altcha-org/altcha v2 is the load-bearing change. If you've pinned the underlying library independently, or written code against its internals, review the upstream changelog — a major bump can change constructor signatures, algorithm names, or the encoded payload format. Verify your stored challenge payload shape in tests, not just the happy path.array cache driver in tests or a per-process driver in production (e.g. file on a multi-worker fleet), single-use silently doesn't protect you. Use Redis or the database.expires). Very slow forms, or users who open a tab and come back the next day, will submit a stale solution. Keep the window generous (a few minutes) unless the form is a login.cost is a UX/security dial, not a firewall. Higher cost slows every legit user. Tune against real low-end mobile hardware, not your dev laptop.cdn.jsdelivr.net for script-src and connect-src (it fetches the challenge endpoint). Self-host the widget bundle if CDN isn't acceptable.How can I help you explore Laravel packages today?