PHPackages                             padosoft/laravel-rebel-bot-protection - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. padosoft/laravel-rebel-bot-protection

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

padosoft/laravel-rebel-bot-protection
=====================================

Pluggable anti-bot / CAPTCHA gate for Laravel Rebel: server-side verification of Cloudflare Turnstile, Google reCAPTCHA v3 and hCaptcha tokens, fail-closed by default and fully audited. Part of padosoft/laravel-rebel-\*.

v0.1.0(1mo ago)04MITPHPPHP ^8.3CI passing

Since Jun 4Pushed 1mo agoCompare

[ Source](https://github.com/padosoft/laravel-rebel-bot-protection)[ Packagist](https://packagist.org/packages/padosoft/laravel-rebel-bot-protection)[ Docs](https://github.com/padosoft/laravel-rebel-bot-protection)[ RSS](/packages/padosoft-laravel-rebel-bot-protection/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (9)Versions (2)Used By (0)

Laravel Rebel — Bot Protection
==============================

[](#laravel-rebel--bot-protection)

> Official documentation:

> **One line of code stands between your login form and the bots.** This package is a pluggable anti-bot / CAPTCHA gate: it takes the little token your CAPTCHA widget produces in the browser, checks it server-side with the provider (Cloudflare Turnstile, Google reCAPTCHA v3 or hCaptcha), and answers a single honest question — *"is this a human?"*. It **fails closed** (blocks on error), records every decision in your audit trail, and never logs the token or your secret. Part of the `padosoft/laravel-rebel-*` suite.

 [![Laravel Rebel](resources/screenshoots/Laravel-Rebel-banner.png)](resources/screenshoots/Laravel-Rebel-banner.png)

 [![Laravel 12|13](https://camo.githubusercontent.com/9e9b743bcbf97a29fe735334a4a8e906d05d60310969905af6607cef8da30138/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313225323025374325323031332d4646324432303f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/9e9b743bcbf97a29fe735334a4a8e906d05d60310969905af6607cef8da30138/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313225323025374325323031332d4646324432303f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c266c6f676f436f6c6f723d7768697465) [![PHP 8.3+](https://camo.githubusercontent.com/6aa777dd33ef43fbef727d8187b578003a61e5dc41bbc958b0938c996cdc92f2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e33253230253743253230382e34253230253743253230382e352d3737374242343f7374796c653d666c61742d737175617265266c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/6aa777dd33ef43fbef727d8187b578003a61e5dc41bbc958b0938c996cdc92f2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e33253230253743253230382e34253230253743253230382e352d3737374242343f7374796c653d666c61742d737175617265266c6f676f3d706870266c6f676f436f6c6f723d7768697465) [![PHPStan max](https://camo.githubusercontent.com/4b9a3c97d76534abb905e64bd9e5bb9f13fe68e962071e0ccbbe7b629112f11c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6d61782d3241364644423f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/4b9a3c97d76534abb905e64bd9e5bb9f13fe68e962071e0ccbbe7b629112f11c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6d61782d3241364644423f7374796c653d666c61742d737175617265) [![Pest 4](https://camo.githubusercontent.com/9b9da1d7d243a7465ab338e9374e47300a7fe2e26b5c291e7e3c95b53153789a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d50657374253230342d3232433535453f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/9b9da1d7d243a7465ab338e9374e47300a7fe2e26b5c291e7e3c95b53153789a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d50657374253230342d3232433535453f7374796c653d666c61742d737175617265) [![fail closed](https://camo.githubusercontent.com/26ec36afe4c852387bb37bbb6db142b8206b6ea267d145ebe438cebf90bb256e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6661696c2d636c6f7365642d3842354346363f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/26ec36afe4c852387bb37bbb6db142b8206b6ea267d145ebe438cebf90bb256e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6661696c2d636c6f7365642d3842354346363f7374796c653d666c61742d737175617265) [![MIT](https://camo.githubusercontent.com/ac049ef4e7a0b7196b09add6ac2d4f180e544c0ac779c2b2ac2fd2723a209579/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75653f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/ac049ef4e7a0b7196b09add6ac2d4f180e544c0ac779c2b2ac2fd2723a209579/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75653f7374796c653d666c61742d737175617265)

---

Table of contents
-----------------

[](#table-of-contents)

- [What it is (and what it is not)](#what-it-is-and-what-it-is-not)
- [Quick glossary (one minute)](#quick-glossary-one-minute)
- [Why this package — the moats](#why-this-package--the-moats)
- [Rebel Bot Protection vs the alternatives](#rebel-bot-protection-vs-the-alternatives)
- [How it works (step by step)](#how-it-works-step-by-step)
- [Installation (junior-proof)](#installation-junior-proof)
- [Where to get the keys](#where-to-get-the-keys)
- [Configuration (every option)](#configuration-every-option)
- [Usage examples](#usage-examples)
- [`.env.example`](#envexample)
- [Telemetry &amp; audit](#telemetry--audit)
- [Security notes](#security-notes)
- [Testing](#testing)
- [🔋 Vibe coding with batteries included](#-vibe-coding-with-batteries-included)
- [License](#license)

---

What it is (and what it is not)
-------------------------------

[](#what-it-is-and-what-it-is-not)

**It is** the *server-side verification half* of a CAPTCHA. Your frontend shows a widget (Turnstile / reCAPTCHA / hCaptcha); the widget hands the browser a short-lived **token**; you send that token to your backend; this package POSTs it to the provider's `siteverify` endpoint with your secret key and turns the answer into a plain `true` / `false`. It implements the core `BotProtection` contract, so the rest of Rebel (Channels, email-OTP, step-up) can call it without knowing or caring which provider you picked.

**It is not** the widget/frontend (you add that with a few lines of HTML per provider — see below), and it is not a WAF or a rate-limiter. It answers exactly one question: *did a human solve the challenge?* Use it as the **first gate** before you spend money on an SMS or send an OTP.

Depends on [`padosoft/laravel-rebel-core`](https://github.com/padosoft/laravel-rebel-core).

---

Quick glossary (one minute)
---------------------------

[](#quick-glossary-one-minute)

TermIn plain words**CAPTCHA**A challenge that's easy for humans, hard for bots ("are you a robot?").**Token / response**The opaque string the widget gives the browser after a challenge. It's single-use and expires fast.**`siteverify`**The provider's server endpoint where you POST `secret` + `token` to get a verdict.**Site key**Public key that goes in your **frontend** widget.**Secret key**Private key used **server-side** here. Never ship it to the browser.**Score (reCAPTCHA v3)**A number in `0.0–1.0`: 1.0 = very likely human, 0.0 = very likely bot. You pick the cutoff.**Fail closed**If the provider can't be reached, **block** the request (the safe default).**Fail open**If the provider can't be reached, **let it through** (availability over security).**Driver**Which provider backs the gate: `turnstile`, `recaptcha`, `hcaptcha`, or `always`.---

Why this package — the moats
----------------------------

[](#why-this-package--the-moats)

★WhatIn short★★★**Provider-agnostic, one contract**Swap Turnstile ↔ reCAPTCHA ↔ hCaptcha by changing **one env var**. Your app code never changes — it talks to the `BotProtection` contract.★★★**Fail-closed by default**A provider outage **blocks** bots instead of waving them through. Flip to fail-open per your risk appetite.★★★**Audited, privacy-first**Every check is recorded in the Rebel audit trail with the IP **HMAC'd** — never the raw token, never your secret.★★**reCAPTCHA v3 score gate**Enforces a configurable `min_score`, and refuses a "successful" v3 response that arrives without a score (no silent accepts).★★**Offline-testable seam**The provider call sits behind a tiny `CaptchaVerifier` seam, so the whole suite runs with the HTTP client faked — zero network.★**Safe default driver**Ships with `always` (no-op) so it installs cleanly and you turn on a real provider when you're ready.---

Rebel Bot Protection vs the alternatives
----------------------------------------

[](#rebel-bot-protection-vs-the-alternatives)

Verifying a CAPTCHA token in a Laravel auth flow, compared:

Capability**Rebel Bot Protection**ShopifyA single-provider package (e.g. one reCAPTCHA wrapper)Hand-rolled `Http::post()`Verify a token server-side✅✅✅✅**Swap provider via one env var** (Turnstile / reCAPTCHA / hCaptcha)✅❌❌➖Fail-closed on provider outage (configurable)✅❌ (hosted black box)➖❌reCAPTCHA v3 `min_score` enforced + no scoreless accepts✅❌➖❌Unified audit trail, IP **HMAC'd**, token never logged✅❌❌❌Plugs into the rest of the auth stack via a shared contract✅❌❌❌Offline test seam (no network in CI)✅❌➖❌Self-hosted / you own the keys &amp; data✅❌✅✅> Legend: ✅ built-in · ➖ partial / manual / per-provider · ❌ not available. A single-provider wrapper is fine until you want to switch vendors or need fail-closed + audit; a hand-rolled `Http::post()` skips the error-handling, scoring and telemetry you'll wish you had after the first incident. Shopify is a closed, hosted commerce platform: it runs its own bot defences on its own login and checkout, but exposes none of these primitives to your Laravel app — you can't choose the provider, set the fail policy, read the audit events, or self-host it. It's a black box for this use case.

---

How it works (step by step)
---------------------------

[](#how-it-works-step-by-step)

```
[browser] CAPTCHA widget  --solves challenge-->  token
     |
     |  (your form POSTs the token to your backend)
     v
$bot->passes($context, $token)
     |
     +-- token missing/empty?            --> record bot.check.failed  --> false (no network call)
     +-- secret not configured?          --> record + apply fail policy
     |
     v
[CaptchaVerifier] POST secret + token  -->  provider /siteverify
     |
     +-- transport/5xx/bad JSON?         --> fail CLOSED (or open) --> record --> result
     +-- success + (score >= min_score)? --> record bot.check.passed --> true
     +-- otherwise                       --> record bot.check.failed --> false

```

Only a real human-solved, server-verified token returns `true`. Everything else — empty token, provider down, low score, bad token — is an audited `false` (unless you opt into fail-open for outages).

---

Installation (junior-proof)
---------------------------

[](#installation-junior-proof)

```
composer require padosoft/laravel-rebel-bot-protection
```

The service provider auto-registers (package discovery). Publish the config if you want to tweak it:

```
php artisan vendor:publish --tag="rebel-bot-protection-config"
```

Then pick a driver in `.env` (default is `always` = no-op, so nothing blocks until you opt in):

```
REBEL_BOT_DRIVER=turnstile
TURNSTILE_SITE_KEY=0x4AAAAAAA...
TURNSTILE_SECRET_KEY=0x4AAAAAAA...
```

That's it — anything in the suite that depends on the `BotProtection` contract now routes through your chosen provider.

---

Where to get the keys
---------------------

[](#where-to-get-the-keys)

ProviderDriver valueDashboardWhat you need**Cloudflare Turnstile**`turnstile`Site Key (frontend) + Secret Key (server)**Google reCAPTCHA v3**`recaptcha` (choose **v3**)Site Key + Secret Key, then tune `min_score`**hCaptcha**`hcaptcha`Site Key + Secret KeyThe **Site Key** goes in your HTML widget; the **Secret Key** stays in `.env` and is used here.

**Minimal frontend snippets** (pair with the matching driver):

```

  grecaptcha.ready(() => grecaptcha.execute('YOUR_SITE_KEY', { action: 'login' })
    .then(token => document.querySelector('#captcha_token').value = token));

```

Whatever the field is named, just hand its value to `passes()` as the `$token`.

---

Configuration (every option)
----------------------------

[](#configuration-every-option)

`config/rebel-bot-protection.php`:

KeyEnvDefaultEffect`driver``REBEL_BOT_DRIVER``always`Active gate: `turnstile` | `recaptcha` | `hcaptcha` | `always`. Unknown value → `always`.`fail_open``REBEL_BOT_FAIL_OPEN``false`On a provider/transport error: `false` = block (fail **closed**), `true` = allow.`turnstile.site_key``TURNSTILE_SITE_KEY`—Public key for the frontend widget (not used server-side).`turnstile.secret``TURNSTILE_SECRET_KEY`—Server secret used to verify the token.`turnstile.endpoint``TURNSTILE_ENDPOINT`Cloudflare siteverify URLOverride for testing / self-hosting.`recaptcha.site_key``RECAPTCHA_SITE_KEY`—Public key for the frontend.`recaptcha.secret``RECAPTCHA_SECRET_KEY`—Server secret.`recaptcha.min_score``RECAPTCHA_MIN_SCORE``0.5`v3 score cutoff (`0.0–1.0`). Below this → bot.`recaptcha.endpoint``RECAPTCHA_ENDPOINT`Google siteverify URLOverride.`hcaptcha.site_key``HCAPTCHA_SITE_KEY`—Public key for the frontend.`hcaptcha.secret``HCAPTCHA_SECRET_KEY`—Server secret.`hcaptcha.endpoint``HCAPTCHA_ENDPOINT`hCaptcha siteverify URLOverride.---

Usage examples
--------------

[](#usage-examples)

### 1. Resolve the gate and check a token (the common case)

[](#1-resolve-the-gate-and-check-a-token-the-common-case)

```
use Padosoft\Rebel\Core\Contracts\BotProtection;
use Padosoft\Rebel\Core\Context\SecurityContext;
use Padosoft\Rebel\Core\Contracts\KeyedHasher;

public function store(Request $request, BotProtection $bot, KeyedHasher $hasher)
{
    $context = SecurityContext::fromRequest($request, $hasher)
        ->withGuard('customers')
        ->withPurpose('customer-login');

    $token = $request->input('cf-turnstile-response'); // or h-captcha-response / your hidden field

    if (! $bot->passes($context, $token)) {
        abort(422, 'Bot check failed. Please retry.');
    }

    // ...human confirmed: proceed to send the OTP / continue login...
}
```

### 2. In a Rebel Channels send (it's the first gate before you spend a cent)

[](#2-in-a-rebel-channels-send-its-the-first-gate-before-you-spend-a-cent)

You don't call it yourself — Channels asks the `BotProtection` contract before dispatching an SMS. Just configure a real driver and the protection is automatic across the suite.

### 3. Local development / tests: opt out explicitly

[](#3-local-development--tests-opt-out-explicitly)

```
REBEL_BOT_DRIVER=always
```

Every request passes, but the gate **still records** a `bot.check.passed` event with `reason: disabled` — so even your "off" state is honest in the audit trail.

### 4. Trade resilience for security during an incident

[](#4-trade-resilience-for-security-during-an-incident)

```
# Provider flaky and you'd rather not block real users? Allow on error:
REBEL_BOT_FAIL_OPEN=true
```

Leave it `false` (the default) for the secure posture: a provider outage blocks suspicious traffic instead of waving it through.

### 5. Bind your own driver (advanced)

[](#5-bind-your-own-driver-advanced)

The contract binding is guarded with `! $app->bound(...)`, so you can register a custom `BotProtection` before this provider boots and it will be used instead — handy for an enterprise provider (e.g. Arkose) without forking the package.

---

`.env.example`
--------------

[](#envexample)

See [`.env.example`](.env.example) for every variable with comments. The essentials:

```
REBEL_BOT_DRIVER=turnstile
REBEL_BOT_FAIL_OPEN=false

TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=

RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
RECAPTCHA_MIN_SCORE=0.5

HCAPTCHA_SITE_KEY=
HCAPTCHA_SECRET_KEY=
```

---

Telemetry &amp; audit
---------------------

[](#telemetry--audit)

Every call to `passes()` records exactly one event through the core `AuditLogger`(persisted to `rebel_auth_events`, never the session):

Event typeWhenMetadata `reason``bot.check.passed`Human verified (or always-pass driver, or fail-open on error)`verified` | `disabled` | `provider_error``bot.check.failed`Bot / bad / missing token, low score, or fail-closed error`rejected` | `missing_token` | `missing_secret` | `provider_error`Each event carries the `provider` (`turnstile` / `recaptcha` / `hcaptcha` / `always`), the request `ip_hmac` and `user_agent_hash` (already HMAC'd by the `SecurityContext`), the `guard` and `purpose`, and — for reCAPTCHA — the numeric `score`. The **raw token and your secret are never recorded**.

---

Security notes
--------------

[](#security-notes)

- **No token/secret leakage**: only a coarse `reason` (and reCAPTCHA `score`) reaches the audit metadata; the token and secret never do.
- **Fail closed by default**: a provider error returns `false`. Opt into `fail_open` consciously.
- **No scoreless accepts (reCAPTCHA v3)**: a `success: true` response without a `score` is treated as a failure — we never accept a v3 token we can't risk-rank.
- **Empty token short-circuits**: a missing token is an immediate audited failure — no pointless network round-trip, no information for an attacker.
- **Privacy by construction**: IP / User-Agent travel as keyed HMACs on the `SecurityContext`; this package only ever records those, never cleartext PII.

---

Testing
-------

[](#testing)

```
composer test      # Pest (providers, HTTP seam, driver selection; live suite self-skips)
composer phpstan   # static analysis, level max
composer pint      # code style
```

The offline suite fakes Laravel's HTTP client — **no network**. The opt-in live suite (`tests/Live`, `REBEL_BOT_LIVE=1`) hits a real `siteverify` endpoint using Cloudflare's documented "always passes" testing keys, and self-skips otherwise.

---

🔋 Vibe coding with batteries included
-------------------------------------

[](#-vibe-coding-with-batteries-included)

This package ships **AI batteries** — so you (and your AI agent) can extend it correctly on the first try:

- **`CLAUDE.md`** — a concise AI working guide (purpose, conventions, architecture, how to extend, Definition of Done). Plain Markdown, so Claude Code, Cursor, Copilot and Codex all read it.
- **`AGENTS.md`** — the agent/workflow contract (branch → PR → CI → tag/release, the gates).
- **`.claude/skills/`** — invocable skills (at least `rebel-package-dev`) encoding the suite's TDD loop, the **PHPStan-level-max** recipes, the security/telemetry rules, and the release discipline.

Open the repo in your AI editor and just start — the rules, guardrails and extension recipes come with it. PRs that follow the shipped `CLAUDE.md` pass CI (PHPStan max + Pest + Pint) and review the first time around.

License
-------

[](#license)

**License:** MIT — see [LICENSE](LICENSE). Part of the [`padosoft/laravel-rebel`](https://github.com/padosoft) suite.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance91

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

51d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/10467699?v=4)[Lorenzo](/maintainers/lopadova)[@lopadova](https://github.com/lopadova)

---

Top Contributors

[![lopadova](https://avatars.githubusercontent.com/u/10467699?v=4)](https://github.com/lopadova "lopadova (4 commits)")

---

Tags

laravelAuthenticationrecaptchacaptchabot-protectionturnstilehcaptchapadosoftRebel

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/padosoft-laravel-rebel-bot-protection/health.svg)

```
[![Health](https://phpackages.com/badges/padosoft-laravel-rebel-bot-protection/health.svg)](https://phpackages.com/packages/padosoft-laravel-rebel-bot-protection)
```

###  Alternatives

[defstudio/telegraph

A laravel facade to interact with Telegram Bots

813336.8k3](/packages/defstudio-telegraph)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

45955.7k](/packages/harris21-laravel-fuse)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1613.3k4](/packages/masterix21-laravel-licensing)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
