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

Symfony Altcha Bundle Laravel Package

druidfi/symfony-altcha-bundle

Symfony bundle that integrates Altcha, a privacy-friendly proof-of-work CAPTCHA alternative, into your forms. Adds spam protection without third-party tracking, cookies, or frustrating image puzzles, with straightforward configuration for Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package actually is: druidfi/symfony-altcha-bundle is a Symfony bundle wrapping altcha-org/altcha and the Altcha web widget. Altcha is a proof-of-work CAPTCHA alternative — no Google, no third-party requests, GDPR-friendly, self-hosted.

Reality check for a Laravel developer: you cannot composer require this bundle and expect it to boot in Laravel. It registers a Bundle class, a Configuration tree, and (as of 1.0.5) Symfony's RateLimiter, none of which Laravel's container resolves. Treat this repo as a reference implementation, and pull in the underlying library directly:

composer require altcha-org/altcha

Where to look first (in the repo, in this order):

  1. DependencyInjection/Configuration.php — the full config surface: HMAC key, algorithm, cost, expiry, and (new in 1.0.5) rate limiting + display mode.
  2. The challenge controller (PR #8 touched both it and the route config) — see exactly what JSON the endpoint returns and what headers it sets.
  3. The service wiring for the Altcha instance — this is where the HMAC key and challenge options are bound.
  4. The Twig/asset integration — mirror it as a Blade component.

First use case: protect login/register. Two moving parts only — a GET endpoint returning a challenge, and a server-side verify call.

// routes/web.php
Route::get('/altcha/challenge', fn () => response()
    ->json(Altcha::createChallenge(['hmacKey' => config('altcha.hmac_key')]))
    ->header('Cache-Control', 'no-store')
)->middleware('throttle:altcha')->name('altcha.challenge');
{{-- resources/views/components/altcha.blade.php --}}
<script src="{{ Vite::asset('node_modules/altcha/dist/altcha.min.js') }}" defer></script>
<altcha-widget challengeurl="{{ route('altcha.challenge') }}" name="altcha"></altcha-widget>
// FormRequest / Rule
Altcha::verifySolution($this->input('altcha'), config('altcha.hmac_key'), true); // true = check expiry

Then add config/altcha.php with hmac_key => env('ALTCHA_HMAC_KEY') and generate the key once with bin2hex(random_bytes(32)).

Implementation Patterns

One widget per form, one hidden field. Name the field altcha everywhere and add a FormRequest rule so it validates uniformly:

protected function prepareForValidation(): void
{
    // keep the payload out of validated(), it is a one-shot token
    $this->merge(['_altcha' => $this->input('altcha')]);
}

Verify in withValidator or a custom Rule object, and strip altcha from validated() before passing to the model.

Challenge endpoint as a stateless GET. This is the pattern the bundle enforces and it maps cleanly to Laravel:

  • No auth, no session, no DB writes.
  • Cache-Control: no-store — the endpoint is per-request by design.
  • Rate limited by IP (see below).
  • Return only the Altcha::createChallenge() array; let the widget consume it.

IP rate limiting (new in 1.0.5). The bundle now throttles the challenge endpoint per client IP. Mirror it — the endpoint does a non-trivial hash loop per hit, so it's a cheap CPU-exhaustion vector:

// app/Providers/AppServiceProvider.php
RateLimiter::for('altcha', fn (Request $r) => [
    Limit::perMinute(30)->by($r->ip()),
    Limit::perMinute(200)->by('altcha:global'),
]);

Keying on $r->ip() is only correct if TrustProxies is configured for your load balancer; otherwise 10k users behind Cloudflare share one bucket and real users get 429s.

Display modes (new in 1.0.5). Display mode is now configurable server-side, so the bundle can render the widget as inline vs. floating/compact without touching templates. In Laravel, express this as a Blade component prop so each form chooses:

<x-altcha display="floating" />

Keep the default inline for accessibility — floating widgets can cover submit buttons on small viewports.

Test helper, not test bypass. Don't disable verification in tests wholesale; you'll never notice a broken form. Instead, pre-solve in the test:

private function solveAltcha(): string
{
    $challenge = Altcha::createChallenge(['hmacKey' => config('altcha.hmac_key')]);
    return Altcha::solveChallenge($challenge['challenge'], ...)[/* base64 payload */];
}

And keep one feature test asserting a 422 for a missing/forged payload.

Replay protection is on you. verifySolution is stateless: the same valid payload can be re-submitted for the lifetime of its expiry. Add Cache::add('altcha:'.$signature, true, $ttl) after successful verification and reject on collision.

Livewire / Inertia specifics. The solution is consumed on first verify, so on a validation failure the user needs a new challenge. In Livewire, dispatch a browser event that calls widget.reset(); in Inertia, force a full reload of the widget after a 422.

Gotchas and Tips

  • Wrong ecosystem. composer require druidfi/symfony-altcha-bundle in a Laravel app pulls symfony/http-kernel and friends and gives you zero working behaviour. Read the source, install altcha-org/altcha.
  • HMAC key mismatch is the #1 failure. Challenge and verification must use the identical key. In Laravel this almost always means a stale bootstrap/cache/config.php after an env change — php artisan config:clear before you debug anything else.
  • Rate limiting + proxies. As above: without TrustedProxy config, $request->ip() is your LB. With a misconfigured trusted proxy list, an attacker can spoof X-Forwarded-For and dodge the limiter entirely. This is the sharpest edge on the 1.0.5 feature.
  • Cache store for the limiter. Symfony's RateLimiter defaults to a cache pool; the file store serializes on locks under load. Use Redis in Laravel — a file-backed limiter on a hot login page becomes its own bottleneck.
  • Never let the challenge endpoint be cached at CDN/proxy level. A shared cached challenge still verifies, but expiry will silently kill submissions from slow typists.
  • Expiry is short by default. Users who park the login tab for a few minutes get a payload that fails verifySolution(..., true) with no visible reason. Either raise expires in the challenge options or reset the widget on every submit attempt.
  • Cost tuning is per-device, not per-server. Altcha's cost parameter is deliberately spent on the client. Test on a low-end Android; a cost that feels instant on your M3 throttles a budget phone for several seconds. The bundle's config exposes algorithm and cost — start low, measure.
  • CSP will break it quietly. The widget spins up a web worker: you need worker-src 'self' blob: and script-src covering your widget source. Symptom is a widget that renders a checkbox and never progresses — browser console shows the worker being blocked, not a PHP error.
  • Debug by decoding the payload. The altcha field is base64-encoded JSON containing algorithm, challenge, salt, signature, number, expires. Decode it first; 90% of "it just doesn't verify" is a key/salt mismatch, not a code bug.
  • Floating/invisible display modes need a resolvable form. The widget finds the enclosing <form> and injects on submit. Custom @submit.prevent handlers, Livewire forms without a real submit button, or a widget placed outside the form will render but never attach.
  • Renovate is now driving dependency bumps (PR #1). Expect frequent minor releases; pin the npm widget version to the range the PHP library supports — a widget major bump against an older PHP verifier produces payloads that fail verification with no useful error.
  • Extension point worth knowing: the challenge controller is overridable and the service is a thin wrapper. If you need to bind extra context into a challenge (form id, user id, tenant), hash it into the salt yourself — but keep using the same HMAC key so verification stays a single call.
  • Not a spam solution by itself. Proof-of-work raises the cost per submission; it does nothing against a human-driven or a farm of real browsers. Pair it with honeypots and rate limits on the actual POST route, not just the challenge endpoint.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings