Panduan implementasi Altcha captcha self-hosted untuk Native PHP dan Laravel/Composer.
Altcha menggunakan metode proof-of-work — tidak ada tracking, tidak ada request ke server eksternal, 100% berjalan di server kamu sendiri.
Server Client (Browser)
| |
|-- generate challenge() ---------->|
| (salt + hmac signature) |
| |-- widget solves proof-of-work
| | (mencari angka yang hash-nya cocok)
|<-- submit payload (base64) --------|
| |
|-- verifySolution(payload) -------->|
| (cek HMAC signature + PoW) |
|-- ✅ valid / ❌ invalid ---------->|
Tidak ada API call eksternal. Semua verifikasi terjadi di server kamu.
Keunggulan implementasi ini: STATELESS + EXPIRY
- Verifikasi tidak bergantung pada session — server hanya perlu memvalidasi HMAC signature dan PoW
- Expiry disimpan di dalam salt (format:
randomhex?expires=TIMESTAMP) — tidak bisa dimanipulasi client - Aman untuk multi-worker, load balancer, dan tidak ada race condition
<!-- Load dari CDN resmi - otomatis dapat update terbaru -->
<script type="module" src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js"></script>Copy native-php/altcha-helper.php ke project kamu.
Di file config atau sebelum require altcha-helper.php:
define('ALTCHA_SECRET', 'isi_dengan_random_string_panjang');Generate secret yang aman:
echo bin2hex(random_bytes(32));<?php
require_once 'altcha-helper.php';
$challenge = generateAltchaChallenge(100000); // complexity default
?><!-- Load script di <head> -->
<script type="module" src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const widget = document.querySelector('altcha-widget');
const submitBtn = document.getElementById('btn-submit');
if (widget && submitBtn) {
// Disable tombol sampai Altcha selesai proof-of-work
submitBtn.disabled = true;
widget.addEventListener('statechange', (ev) => {
if (ev.detail.state === 'verified') {
document.getElementById('altcha_input').value = ev.detail.payload;
submitBtn.disabled = false;
} else if (ev.detail.state === 'expired' || ev.detail.state === 'error') {
// Challenge expired — reload untuk generate challenge baru
submitBtn.disabled = true;
submitBtn.textContent = 'Captcha expired, memuat ulang...';
setTimeout(() => location.reload(), 2000);
} else {
submitBtn.disabled = true;
document.getElementById('altcha_input').value = '';
}
});
}
});
</script>
<!-- Di dalam <form> -->
<altcha-widget
challengeurl="data:application/json;base64,<?= base64_encode(json_encode($challenge)) ?>"
hidefooter
hidelogo
></altcha-widget>
<input type="hidden" name="altcha" id="altcha_input">
<!-- Tambahkan id="btn-submit" pada tombol submit -->
<button type="submit" id="btn-submit">Login</button>Kenapa disable button + handle expired? Tombol di-disable sampai Altcha selesai proof-of-work. Jika challenge expired (user diam 15+ menit), handler
expiredotomatis reload halaman untuk generate challenge baru — tidak perlu refresh manual.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$payload = $_POST['altcha'] ?? '';
if (!verifyAltchaSolution($payload)) {
$error = 'Verifikasi captcha gagal. Silakan coba lagi.';
} else {
// Lanjutkan proses login/register
}
}Lihat file native-php/login-example.php.
Copy laravel/AltchaService.php ke app/Services/AltchaService.php.
Opsi A — via database settings table:
// Jalankan sekali lewat tinker
php artisan tinker --execute="
App\Models\Setting::create([
'key' => 'altcha_secret',
'value' => bin2hex(random_bytes(32)),
]);
"Opsi B — via .env:
ALTCHA_SECRET=isi_dengan_random_string_panjangLalu di config/app.php tambahkan:
'altcha_secret' => env('ALTCHA_SECRET'),use App\Services\AltchaService;
public function showLogin()
{
$challenge = AltchaService::generateChallenge(); // complexity default 100000
return view('auth.login', compact('challenge'));
}{{-- Di <head> --}}
@if($challenge)
<script type="module" src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const widget = document.querySelector('altcha-widget');
const submitBtn = document.getElementById('btn-submit');
if (widget && submitBtn) {
// Disable tombol sampai Altcha selesai proof-of-work
submitBtn.disabled = true;
widget.addEventListener('statechange', (ev) => {
if (ev.detail.state === 'verified') {
document.getElementById('altcha_input').value = ev.detail.payload;
submitBtn.disabled = false;
} else if (ev.detail.state === 'expired' || ev.detail.state === 'error') {
// Challenge expired — reload untuk generate challenge baru
submitBtn.disabled = true;
submitBtn.textContent = 'Captcha expired, reloading...';
setTimeout(() => location.reload(), 2000);
} else {
submitBtn.disabled = true;
document.getElementById('altcha_input').value = '';
}
});
}
});
</script>
@endif
{{-- Di dalam form --}}
@if($challenge)
<altcha-widget
challengeurl="data:application/json;base64,{{ base64_encode(json_encode($challenge)) }}"
hidefooter
hidelogo
></altcha-widget>
<input type="hidden" name="altcha" id="altcha_input">
@endif
{{-- Tambahkan id="btn-submit" pada tombol submit --}}
<button type="submit" id="btn-submit">Login</button>Kenapa disable button + handle expired? Tombol di-disable sampai Altcha selesai proof-of-work. Jika challenge expired (user diam 15+ menit), handler
expiredotomatis reload halaman untuk generate challenge baru — tidak perlu refresh manual.
public function login(Request $request)
{
// Verifikasi captcha
if (!AltchaService::verifySolution($request->input('altcha'))) {
return back()->withErrors([
'email' => 'Verifikasi captcha gagal. Silakan coba lagi.'
])->withInput();
}
// Lanjutkan proses login...
}- Controller:
laravel/AuthController-example.php - Blade view:
laravel/login-example.blade.php
| Atribut | Fungsi |
|---|---|
challengeurl |
URL atau data URI challenge JSON |
hidefooter |
Sembunyikan footer "Protected by Altcha" |
hidelogo |
Sembunyikan logo Altcha |
auto="onload" |
Auto-solve tanpa klik user |
floating |
Mode floating widget |
Contoh dengan auto-solve (invisible captcha):
<altcha-widget
challengeurl="..."
auto="onload"
hidefooter
hidelogo
></altcha-widget>Secret Key
- Gunakan minimal 32 karakter random:
bin2hex(random_bytes(32)) - Simpan di database atau
.env, jangan hardcode di source code - Jangan commit secret ke repository
Complexity
50000— ringan, cocok untuk server kecil100000— default, balance antara keamanan dan kecepatan200000— lebih aman, sedikit lebih lambat di client
Stateless + Expiry
- Verifikasi via HMAC signature — tidak butuh session
- Expiry disimpan di salt:
randomhex?expires=TIMESTAMP— client tidak bisa manipulasi - Challenge berlaku 15 menit, auto-reload jika expired
- Secret key auto-rotate setiap 24 jam (trigger by request, tanpa crontab)
- Aman untuk multi-worker, load balancer, tidak ada race condition
Kombinasi dengan Rate Limit Altcha mencegah bot otomatis, tapi tetap kombinasikan dengan rate limit by email/username untuk perlindungan berlapis — terutama jika digunakan di Tor hidden service (jangan rate limit by IP).
MIT — bebas digunakan dan dimodifikasi.