Search by

danielm / laravel-simple-altcha

danielm

ALTCHA (proof-of-work captcha) for Laravel: challenge endpoint, validation rule, middleware and Inertia/React stubs. Built on altcha-org/altcha v2.

Package info

github.com/danielm/laravel-simple-altcha

pkg:composer/danielm/laravel-simple-altcha

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.2 2026-10-04 18:58 UTC

This package is auto-updated.

Last update: 2026-10-04 18:58:38 UTC


README

tests

ALTCHA proof-of-work captcha for Laravel, built on altcha-org/altcha v2. Includes:

  • a challenge endpoint (GET /altcha/challenge)
  • a ValidAltcha validation rule and an altcha route middleware
  • replay protection (a solved payload works once)
  • a React / Inertia component published as a stub

Install

composer require danielm/laravel-simple-altcha
npm install altcha

.env:

# php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
ALTCHA_HMAC_SECRET=

All supported variables are documented in .env.example.

Optional publishing:

php artisan vendor:publish --tag=altcha-config   # config/altcha.php
php artisan vendor:publish --tag=altcha-lang     # translations
php artisan vendor:publish --tag=altcha-react    # React component + JSX types

The altcha-react tag copies:

  • resources/js/components/altcha-widget.tsx
  • resources/js/types/altcha.d.ts

Backend

Validation rule (implicit, so a missing field also fails):

use Danielm\LaravelSimpleAltcha\Rules\ValidAltcha;

$request->validate([
    'email'  => ['required', 'email'],
    'altcha' => [new ValidAltcha],
]);

Or middleware:

Route::post('/contact', ContactController::class)->middleware('altcha');

Or directly:

$result = app(\Danielm\LaravelSimpleAltcha\AltchaManager::class)->verify($request->input('altcha'));
$result->verified;  // bool
$result->reason;    // missing | malformed | expired | invalid | replayed

Frontend (Inertia + React)

import { useRef } from 'react';
import { useForm } from '@inertiajs/react';
import { AltchaWidget, type AltchaHandle } from '@/components/altcha-widget';

export default function Contact() {
    const altcha = useRef<AltchaHandle>(null);
    const { data, setData, post, processing, errors } = useForm({ email: '', altcha: '' });

    const submit = (e: React.FormEvent) => {
        e.preventDefault();
        post('/contact', {
            // payloads are single-use, so always get a fresh one afterwards
            onFinish: () => altcha.current?.reset(),
        });
    };

    return (
        <form onSubmit={submit}>
            <input value={data.email} onChange={(e) => setData('email', e.target.value)} />
            <AltchaWidget ref={altcha} onChange={(payload) => setData('altcha', payload)} />
            {errors.altcha && <p>{errors.altcha}</p>}
            <button disabled={processing || !data.altcha}>Send</button>
        </form>
    );
}

challengeUrl defaults to /altcha/challenge. If you change altcha.route.path, pass the new URL. Extra props are forwarded to <altcha-widget> as attributes.

Configuration notes

  • Algorithm: pbkdf2 (default, no extensions), argon2id (ext-sodium), scrypt (ext-scrypt). cost means different things per algorithm. Your widget build must support the one you choose.
  • Replay protection uses Cache::add(). Use redis/memcached/database in production; the file driver is not atomic. Keys are derived from the challenge's HMAC signature, so re-encoding a payload does not bypass it.
  • Challenge route is intentionally outside the web group (no session or cookies) and throttled by default. Adjust altcha.route.middleware.
  • Widget requires HTTPS (localhost is fine for development).

Testing in your app

Set ALTCHA_TESTING_BYPASS=let-me-in in .env.testing, then post ['altcha' => 'let-me-in']. The bypass only works when the app environment is testing.

Package tests

composer install
composer test