rafalmasiarek / dashboard-kit-addon-captcha
CAPTCHA addon for rafalmasiarek/dashboard-kit — injects CAPTCHA (reCAPTCHA, Cloudflare Turnstile, hCaptcha, via rafalmasiarek/captcha) into login and register forms, with DB-backed failed-attempt throttling.
Package info
github.com/rafalmasiarek/php-dashboard-kit-addon-captcha
pkg:composer/rafalmasiarek/dashboard-kit-addon-captcha
Requires
- php: >=8.2
- rafalmasiarek/captcha: ^2.0
- rafalmasiarek/dashboard-kit: ^5.4.2
- rafalmasiarek/real-ip-resolver: ^2.3
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-08 17:16:12 UTC
README
CAPTCHA addon for rafalmasiarek/dashboard-kit — injects reCAPTCHA (v2 or v3), Cloudflare Turnstile, or hCaptcha into the login and register forms, verified through rafalmasiarek/captcha.
Supersedes rafalmasiarek/dashboard-kit-addon-recaptcha: same wiring mechanism, generalized to any provider, with the login x-failed-attempts counter moved from a PHP session (trivially reset by an attacker who simply never retains a session cookie between attempts) to a DB-persisted, IP-keyed counter.
Install
composer require rafalmasiarek/dashboard-kit-addon-captcha
Usage
use rafalmasiarek\DashboardKitCaptcha\CaptchaAddon; CaptchaAddon::register($app, $container, [ 'provider' => 'recaptcha_v2', // or 'recaptcha_v3', 'turnstile', 'hcaptcha' 'site_key' => $siteKey, 'secret_key' => $secretKey, 'login' => ['mode' => 'x_failed', 'threshold' => 3], 'register' => true, ]);
Or via app.config['captcha'] if already bound in the container — register()'s $config argument is merged over it.
Config
| Key | Scope | Meaning |
|---|---|---|
provider |
both | recaptcha_v2 | recaptcha_v3 | turnstile | hcaptcha, or your own CaptchaProviderInterface instance — see below |
widget |
both | Required only when provider is a CaptchaProviderInterface instance — the matching CaptchaWidgetDescriptor |
site_key, secret_key |
both | Provider credentials — secret_key only required for a built-in shorthand provider; a custom provider already carries its own |
login |
— | false (default) to disable, true to always show, or an array (below) |
login.mode |
login | 'always' (default) or 'x_failed' |
login.threshold |
login | Failed attempts before the widget appears, in x_failed mode (default 3) |
login.ttl_seconds |
login | How long a failed-attempt count survives without a fresh failure (default 900) |
login.min_score, login.action |
login | reCAPTCHA v3 / hCaptcha Enterprise only — no-op otherwise |
register |
— | false (default), true, or an array with min_score/action |
An invisible widget (recaptcha_v3) has no checkbox to conditionally show, so x_failed mode has no effect on it — it always runs.
Using a provider this addon doesn't know about
The four shorthand strings are convenience only — CaptchaAddon never needs editing to support a fifth provider, the same extensibility rafalmasiarek/captcha itself offers:
use rafalmasiarek\DashboardKitCaptcha\CaptchaAddon; CaptchaAddon::register($app, $container, [ 'provider' => new MyCustomProvider($secretKey), // implements CaptchaProviderInterface 'widget' => MyCustomProvider::widget(), // your own CaptchaWidgetDescriptor factory 'site_key' => $siteKey, 'login' => ['mode' => 'x_failed', 'threshold' => 3], ]);
secret_key isn't needed in this form — your provider already carries its own.
Why the failed-attempt counter moved to the database
The superseded addon counted failed logins in $_SESSION. That throttles a human who keeps retyping their password in the same browser tab — it does nothing against an automated attacker, who simply never sends (or keeps) a session cookie between requests; every attempt then starts the counter at zero. FailedAttemptTracker instead keys by the caller's real IP (resolved the same way Captcha itself resolves it — see below), with the PHP session id only as a last-resort fallback when no IP is available at all. The counter is a small table (captcha_failed_attempts), created automatically via dashboard-kit's own schema-sync mechanism, with a configurable TTL so a stale count doesn't linger indefinitely.
IP resolution
If rafalmasiarek\RealIpResolver::class is bound in the container, it's bridged to rafalmasiarek/captcha's RemoteIpProviderInterface via RealIpResolverAdapter and used both as Captcha's default IP source and as the failed-attempt counter's key. Otherwise, both fall back to the same naive $_SERVER['REMOTE_ADDR'] (via rafalmasiarek/captcha's own SystemRemoteIpProvider) — the addon and the library always agree on which IP they're looking at.
Pluggable failed-attempt storage
FailedAttemptStoreInterface (increment()/count()/reset(), TTL-aware) is the seam — FailedAttemptTracker (the default, via dashboard-kit's Model) works with whatever PDO driver the app already configured. A different storage backend (e.g. Redis, for a multi-server deployment without a shared database) can implement the same interface.
Using CaptchaService in your own modules
CaptchaAddon::register() builds one CaptchaService from the app's single ['captcha'] config and binds it in the container — not just for login/register. Any module that wants its own CAPTCHA (a contact form, a newsletter signup, whatever) injects the same service instead of re-deriving provider/widget/site-key wiring itself:
use rafalmasiarek\DashboardKitCaptcha\CaptchaService; $captchaService = $container->get(CaptchaService::class);
Rendering (give each widget on the page its own instanceId — see rafalmasiarek/captcha's own README for why):
echo $captchaService->widget(instanceId: 'contactform'); echo $captchaService->scripts(action: 'contact_submit', instanceId: 'contactform');
Verifying — captchaFor() returns a real rafalmasiarek\Captcha\Captcha, configured with the addon's provider/site/secret but your own $container label (for log output) and, optionally, your own $expectedAction/$minScore:
$captcha = $captchaService->captchaFor('contactform', expectedAction: 'contact_submit'); $token = $request->getParsedBody()[$captchaService->tokenFieldName()] ?? ''; $result = $captcha->verify($token, $remoteIp);
CaptchaService holds no provider-specific knowledge of its own — it's a thin binding of rafalmasiarek/captcha's own Captcha/HtmlHelper/CaptchaWidgetDescriptor to whatever this addon resolved from config, so a module using it gets the exact same behavior (fail-closed score/action checks, the captcha:error client-side event, CSP $nonce support, ...) rafalmasiarek/captcha ^2.0 itself provides — see its own README for the full client-side contract.
License
BUSL-1.1