tito10047/altcha-bundle
A Symfony bundle that integrates ALTCHA, a privacy-friendly proof-of-work CAPTCHA alternative, into your forms. Protect submissions from spam and bots without third-party tracking, cookies, or external services, with simple configuration and customizable widget options.
Read this first: tito10047/altcha-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/altcha.php, no Blade directives. As a Laravel developer you get value from it in one of two ways:
altcha-org/altcha-lib-php provides Altcha::createChallenge() / Altcha::verifySolution(). This is what the bundle wraps, and it is framework-agnostic.Do not try to composer require the bundle into a Laravel app; you will pull in symfony/http-kernel contracts and get nothing usable.
Minimal path in a Laravel app:
composer require altcha-org/altcha-lib-php
ALTCHA_HMAC_KEY to .env (a long random string, php artisan key:generate-style secret — never reuse APP_KEY).GET /altcha/challenge → controller returns Altcha::createChallenge(['hmacKey' => config('services.altcha.key')]) as JSON.<altcha-widget challengeurl="/altcha/challenge"></altcha-widget> plus the widget script.ValidAltcha validation rule decodes the posted altcha field and calls Altcha::verifySolution(...).First use case: a public contact form or registration form. ALTCHA is a proof-of-work CAPTCHA — no Google dependency, no user puzzle, no PII shipped to a third party. It is the right default when GDPR/privacy review is blocking reCAPTCHA, and it costs one HMAC verify per submit.
Where to look in the 3.1.0 source: the challenge route/controller, the request listener that verifies submissions, the Twig widget template (for the exact attribute names and nonce placement), and the README's CSP section.
Custom validation rule (the workhorse pattern). Implement ValidationRule rather than a middleware for form-level checks, so failures surface as normal field errors:
final class ValidAltcha implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || $value === '') {
$fail('Please complete the verification.');
return;
}
$result = Altcha::verifySolution($value, config('services.altcha.key'), true);
if (! ($result['verified'] ?? false)) {
$fail('Verification failed. Please try again.');
}
}
}
Attach as 'altcha' => ['required', new ValidAltcha] on the form request. Keep the field required — a missing field is the single most common false positive in logs.
Blade component, one place to change. Wrap <altcha-widget> plus its script tag in resources/views/components/altcha.blade.php, accepting :challengeurl, :auto, :nonce. Every form then uses <x-altcha />, and CSP nonce plumbing (new in 3.1.0) lives in exactly one file.
Invisible mode for low-friction forms. The widget supports auto-solving — configure it to solve on load (login, search) or on submit (comment forms). Prefer on-load for pages where the user will type for a while; the proof-of-work finishes while they type.
Middleware for high-risk endpoints. For /register, /password/email, /login where you want a hard gate before the form request even runs, verify in middleware and unset() the altcha input so it never reaches mass-assignment.
Tune maxnumber to your audience. This is the difficulty knob. Target roughly 1–2 seconds on a mid-range phone. Set it per-route in config (login can be harder than a newsletter signup) and expose it through the challenge endpoint, not hardcoded in the Blade view.
CSP wiring (the 3.1.0 headline). The widget builds a Web Worker from a Blob, so a strict policy needs:
script-src — the widget script, with your Laravel nonce.worker-src blob: (or child-src blob: for older browsers) — without this the widget renders but never solves.connect-src — your challengeurl origin, if it isn't same-origin.Pass the nonce through to the component; 3.1.0's bundle work is exactly the pattern to mirror.
Testing. Bind a fake verifier interface in the container and assert your rule calls it, then unit-test one real HMAC round trip against altcha-lib-php to catch payload-format drift. Http::fake() the challenge endpoint in feature tests instead of generating real proofs (they're deliberately slow).
Pin >= 3.1.0 if you vendor or copy bundle code. 3.1.0 fixes a TypeError (#70) thrown on payload shape — i.e. exactly the malformed/absent-payload case a public endpoint receives. If you copy verification logic from 3.0.0, harden it: check is_string, check the decoded JSON has algorithm, challenge, number, salt, signature before passing it on.
A widget that hangs is almost always CSP. Silent non-solving, no console error, form never submits. Check worker-src blob: first, then that the script tag carries the nonce. This is the reason 3.1.0's CSP support matters.
Don't cache or reuse verifications. The payload carries an expiry and a salt; verification is intentionally one-shot. Don't memoize by payload string in a cache layer, and don't verify in a queued job — it's a request-time gate.
Rate-limit the challenge endpoint. It's cheap, but it's public and unauthenticated. RateLimiter::for('altcha', fn ($r) => Limit::perMinute(30)->by($r->ip())).
Livewire / Inertia / SPA re-renders break the widget. It's a web component that initialises once. After a Livewire morph or an Inertia visit, the element can persist in a stale state — call the widget's reset() in a hook, or key the element so it remounts.
Verification failure vs. CSRF/session failure look identical to users, not to you. Look at the network tab first: a 419 is not an ALTCHA problem. A genuine failure returns verified: false along with expired/invalid flags — log those flags, they tell you whether the clock skew or the difficulty is the issue.
Keep the HMAC key server-side and rotate deliberately. Rotating invalidates only in-flight challenges — harmless. Leaking it lets anyone mint valid payloads.
Accessibility and fallback. Provide a visible error state and let the user retry; proof-of-work can fail on very old devices or with aggressive battery savers. Do not make the submit button permanently disabled with no explanation.
Extension point to copy from the bundle: it isolates verification behind a dedicated service. Do the same — a AltchaVerifier contract bound in a service provider — so your validation rule and middleware share one implementation and tests can swap it.
How can I help you explore Laravel packages today?