A local Proof-of-Work CAPTCHA extension for Flarum that protects login, registration and password-reset forms without relying on any external service (no Cloudflare, no Google reCAPTCHA, no cookies).
- When an auth modal opens, the browser silently asks the Flarum API for a challenge for that specific form (login, sign-up, password reset).
- The server returns a signed, stateless challenge. Its difficulty, issue time and form are protected by an HMAC, so the browser cannot alter them and the server stores nothing at this point.
- The browser solves a SHA-256 puzzle (finds a nonce N such that
SHA-256(challenge:N)starts with D hex zeros). - The token
challenge:nonceis sent with the form. The server checks the signature, the form, the age, the work done and that the token has never been used (atomically), and rejects the request otherwise.
Bots must repeat the work for every single request, and the work grows automatically for networks that misbehave (see Adaptive difficulty).
- ๐ No external services โ fully self-hosted, no cookies
- โก Invisible to users โ solved in the background while they fill in the form
- ๐งฎ Three difficulty levels โ plus adaptive difficulty that raises the work (up to +2 steps) for networks with repeated failures or many sign-ups, and +1 for everyone during a site-wide attack
- โ๏ธ Signed stateless challenges โ difficulty and form cannot be forged; a login token cannot be used on the sign-up form
- ๐ Atomic single use โ a token can be consumed only once, even with parallel requests (uses a cache lock when available)
- ๐ชค Honeypot and time trap โ invisible traps for form-filling bots (sign-up must take at least 2 s)
- ๐ก๏ธ Optional protection of
/api/tokenโ the public API login endpoint that bypasses the login form - ๐ Anonymous event log in the admin panel โ last 30 passed / 30 failed checks, no IP address or personal data, self-pruning, with a Clear log button
- โฟ Accessible โ polite live-region announcements, no visual puzzle, no dragging, honeypot hidden from assistive technology
- ๐ Italian & English, ๐ dark / light mode, โ Flarum 2.x (the PHP side also keeps a Flarum 1.x code path, but the shipped JavaScript is built for 2.x)
| Dependency | Version |
|---|---|
| PHP | โฅ 8.1 |
| Flarum | ^1.8 or ^2.0 |
This software is provided "AS IS", without any warranty. While it has been tested and reasonable efforts are made to ensure security and reliability, no guarantees are provided. As an open project, anyone may contribute or report issues, but this does not imply endorsement or liability from the maintainers.
You use this software entirely at your own risk. The authors and contributors are not liable for any damages, data loss, or unexpected behavior resulting from its use, modification, or distribution. Always review and test the code independently before deploying it in critical or production environments.
composer require peopleinside/fla-powcaptchaFor Flarum 1.8 use the version 2.3
The future will be Flarum 2.0 that support the latest release.
Install with
composer require peopleinside/fla-powcaptcha:2.3composer update peopleinside/fla-powcaptcha
php flarum migrate
php flarum cache:clearmigrate is required: it creates the log table and converts the old difficulty setting (see Upgrading from 3.7.x).
composer remove peopleinside/fla-powcaptchaThen enable the extension in the Flarum admin panel.
Go to Admin โ Extensions โ PoW CAPTCHA.
| Setting | Default | Description |
|---|---|---|
| Enable on Login / Registration / Password Reset | on | Which forms are protected |
| Difficulty level | 2 โ Balanced | Level 1 = 3 hex zeros (~4k hashes), level 2 = 4 (~65k), level 3 = 5 (~1M). Approximate time on a recent PC: instant / โ 0.1 s / โ 1โ3 s (phones are 3โ6ร slower) |
| Adaptive difficulty | on | Raises the difficulty for suspicious networks (hard ceiling: 6 zeros) |
| Protect the API login endpoint | on for new installs, off for existing ones | Requires a CAPTCHA token on direct POST /api/token calls |
| Keep an anonymous event log | on | Last 30 passed + 30 failed checks, no personal data |
The old difficulty setting (3/4/5) is converted once, by a migration, to the new level (1/2/3) with exactly the same real difficulty: old 1 or 3 โ level 1, old 2 or 4 โ level 2, old 5 โ level 3. The old value is left in the database, so downgrading still works. Run php flarum migrate and php flarum cache:clear after updating.
Flarum's login form posts to /login, which calls /api/token internally, so a bot that calls /api/token directly never meets the login CAPTCHA. With this option on, direct calls must include captchaToken (and no value in pow_website) obtained from GET /api/powcaptcha/challenge?action=token. Turn it off if mobile apps or integrations log in through the API. It only takes effect on Flarum core versions that mark internal API calls (RequestUtil::isInternal); on older cores it is simply inactive, never blocking the normal form. Flarum's own login throttler (5 failed attempts per minute per account and IP) keeps applying in all cases.
The extension ships with pre-compiled frontend assets (js/dist/*), so no build step is needed to install or use it. Always rebuild js/dist before tagging a release so the shipped bundle matches js/src:
cd js
npm ci
npm run buildThe PHP logic is covered by plain-PHP tests (no Flarum installation required): php tests/run.php. After every build run bash tests/js/run.sh (Node only): it loads the shipped bundles the way Flarum does and catches boot-time crashes before they reach a forum.
- Challenges are valid for 5 minutes, signed with HMAC-SHA256 using a per-installation secret (created on install, stored in the settings table), and single use.
- The server recomputes the hash itself; difficulty and form come from the signed challenge, never from the client.
- Fail-closed: if the cache is unavailable, verification fails instead of letting requests through.
- Challenges issued to networks that already look abusive are bound to that network, so they cannot be requested from a clean IP and used from a dirty one. Normal visitors are never bound (no problems with dual-stack or mobile networks).
- Challenge requests are rate limited (30 per minute per IPv4 address or IPv6 /64).
- Limits you should know about: Proof-of-Work raises the cost of every request but does not tell humans from bots, and it does not stop a determined attacker with a headless browser or a native SHA-256 solver. Combine it with e-mail confirmation, approval of first posts and reputation checks. On busy sites prefer Redis / APCu / Memcached over the
filecache: the file driver does not delete expired entries on its own.
The log stores only time, form, outcome, a technical reason and difficulty. It never stores IP addresses (not even truncated or hashed), user agents, usernames or e-mail addresses. Abuse counters used for rate limiting and adaptive difficulty live in the cache, are keyed by an HMAC of the network (never the raw IP) and expire within minutes. Even so, mention the CAPTCHA in your privacy policy; this is not legal advice.
Important: this extension (like Flarum core) uses the connection's REMOTE_ADDR as the client address and deliberately ignores X-Forwarded-For, Client-IP and similar headers, which anyone can forge. Flarum has no proxy_headers / proxy_all config keys (older versions of this README suggested them mistakenly โ they have no effect on core and must not be relied on).
If you use a reverse proxy or a CDN, configure the web server so REMOTE_ADDR carries the real visitor address, for example:
- nginx:
set_real_ip_from <proxy CIDR>; real_ip_header X-Forwarded-For;(orCF-Connecting-IPfor Cloudflare) withreal_ip_recursive on; - Apache:
mod_remoteipwithRemoteIPHeaderandRemoteIPTrustedProxy
If REMOTE_ADDR is a private or reserved address (typical when that is not configured), the per-IP features โ rate limit, adaptive difficulty and network binding โ are switched off automatically instead of treating all visitors as one; the CAPTCHA itself keeps working.
The forum still seems to run old code after an update (typical when the version number stays the same, because Composer may serve its cached copy):
- Replace the extension folder completely (delete it, then copy the whole content of the release); do not overlay files onto the old ones.
- Run
composer clear-cache, thencomposer dump-autoloadandphp flarum cache:clearfrom the Flarum root. - Check the server side:
ls vendor/peopleinside/fla-powcaptcha/srcmust listProvider,Middleware,Service,Controllerโฆ, andextend.phpmust containautoload.php(grep -c autoload.php vendor/peopleinside/fla-powcaptcha/extend.phpโ1). - Check the browser side: open the forum in a private window, press F12 and type
flarum.extensions['peopleinside-fla-powcaptcha']. It must answer{build: "3.8.0+โฆ"}; if it does not, the old JavaScript is still being served.
Class "PeopleInside\PowCaptcha\โฆ" not found in storage/logs: the files are incomplete or Composer's classmap is stale. Follow the steps above.
A form is broken or empty: in Admin โ Extensions โ PoW CAPTCHA switch off the CAPTCHA for that form. If the problem remains, it is not caused by this extension.