PHPackages                             returnearly/laravel-cloudflare-zero-trust - 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. returnearly/laravel-cloudflare-zero-trust

ActiveLibrary

returnearly/laravel-cloudflare-zero-trust
=========================================

Security-first Cloudflare Access (Zero Trust) authentication for Laravel

10PHPCI passing

Since Aug 6Pushed todayCompare

[ Source](https://github.com/returnearly/laravel-cloudflare-zero-trust)[ Packagist](https://packagist.org/packages/returnearly/laravel-cloudflare-zero-trust)[ RSS](/packages/returnearly-laravel-cloudflare-zero-trust/feed)WikiDiscussions master Synced today

READMEChangelog (2)DependenciesVersions (1)Used By (0)

Cloudflare Zero Trust for Laravel
=================================

[](#cloudflare-zero-trust-for-laravel)

 [![Cloudflare Zero Trust for Laravel — security-first Access authentication](.github/social-preview.png)](.github/social-preview.png)

[![Latest Version on Packagist](https://camo.githubusercontent.com/bee434f2297c4f01fe0c688ba506eb2ea0024f05fc77ce69d3ef35407dd4a54a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f72657475726e6561726c792f6c61726176656c2d636c6f7564666c6172652d7a65726f2d74727573742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/returnearly/laravel-cloudflare-zero-trust)[![GitHub Tests Action Status](https://camo.githubusercontent.com/e3f8c64aa920a1ce2a0aa4d370568921fc7526dac184d92d632e3f42a590f83d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f72657475726e6561726c792f6c61726176656c2d636c6f7564666c6172652d7a65726f2d74727573742f72756e2d74657374732e796d6c3f6272616e63683d6d6173746572266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/returnearly/laravel-cloudflare-zero-trust/actions?query=workflow%3Arun-tests+branch%3Amaster)[![PHPStan](https://camo.githubusercontent.com/e80fa2b862f49169aef9a96fb4946afa2f50ddcd1dafa6aeadd431dceb66441e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f72657475726e6561726c792f6c61726176656c2d636c6f7564666c6172652d7a65726f2d74727573742f7068707374616e2e796d6c3f6272616e63683d6d6173746572266c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/returnearly/laravel-cloudflare-zero-trust/actions?query=workflow%3Aphpstan+branch%3Amaster)[![Total Downloads](https://camo.githubusercontent.com/30916772912cc1af216c5951413b2b04985a84647c9a82c823d7410b17e6ab70/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f72657475726e6561726c792f6c61726176656c2d636c6f7564666c6172652d7a65726f2d74727573742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/returnearly/laravel-cloudflare-zero-trust)

Security-first [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) (Zero Trust) authentication for Laravel.

When you put an application behind Cloudflare Access, Cloudflare authenticates every request at the edge — via your identity provider for humans, or via service tokens for machines — and forwards a signed JWT to your origin in the `Cf-Access-Jwt-Assertion` header. This package verifies that JWT cryptographically and turns it into a first-class Laravel authentication primitive:

- **Route middleware** that fails closed: signature, issuer, audience, expiry, and claim-shape checks before anything is trusted
- **Typed principals** — SSO users and service tokens are distinct, verified identities, not loosely-inspected claims
- **Multiple accounts and applications** — protect your app, your API, and Horizon with different Access policies from one config file
- **A Laravel auth guard** so `auth()->user()` works, optionally resolving Access users onto your own `User` model
- **On-demand identity enrichment** — groups, IdP attributes, and device posture fetched lazily and cached
- **Cached, rate-limited JWKS handling** with automatic key-rotation recovery
- **Events for observability** on every authentication success and failure
- **Request-scoped state** — safe under Laravel Octane and other long-lived workers

How it works
------------

[](#how-it-works)

```
Browser / API client
        │
        ▼
Cloudflare Access (edge)  ── authenticates via IdP SSO or service token
        │
        ▼  adds Cf-Access-Jwt-Assertion:
Your Laravel origin
        │
        ▼
cloudflare-access middleware
   1. Reads the JWT from the assertion header
   2. Verifies RS256 signature against your team's JWKS (cached)
   3. Validates issuer, audience, expiry, and token type
   4. Classifies the request as a user or service principal
   5. Rejects with 401/403 — or lets the request through

```

The package never trusts unverified input: principal classification happens only *after* cryptographic verification, and `CF-Access-Client-Id` / `CF-Access-Client-Secret` headers are never treated as origin credentials (Cloudflare validates those at the edge and mints a signed JWT for your origin).

Requirements
------------

[](#requirements)

- PHP 8.3, 8.4, or 8.5
- Laravel 12 or 13
- An application protected by Cloudflare Access (typically via [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/))

Installation
------------

[](#installation)

```
composer require returnearly/laravel-cloudflare-zero-trust
```

Publish the config file:

```
php artisan vendor:publish --tag="cloudflare-zero-trust-config"
```

Quick start
-----------

[](#quick-start)

**1.** Find your **team domain** (Cloudflare Zero Trust dashboard → Settings → Custom Pages) and the **Application Audience (AUD) tag** for each Access application (Access → Applications → your app → Overview).

**2.** Define your accounts and applications in `config/cloudflare-zero-trust.php`:

```
use ReturnEarly\CloudflareZeroTrust\Enums\PrincipalKind;

'accounts' => [
    'acme' => [
        'team_domain' => 'https://acme.cloudflareaccess.com',
        'applications' => [
            'internal-api' => [
                'audience' => [env('CLOUDFLARE_INTERNAL_API_AUD')],
                'enabled' => (bool) env('CLOUDFLARE_INTERNAL_API_ENABLED', true),
                'principals' => [PrincipalKind::User, PrincipalKind::Service],
            ],
        ],
    ],
],
```

**3.** Protect your routes:

```
Route::middleware('cloudflare-access:internal-api')->group(function () {
    Route::get('/reports', ReportController::class);
});
```

**4.** Use the verified identity:

```
use ReturnEarly\CloudflareZeroTrust\Support\Access;

Access::user()?->email();         // "jane@example.com" for SSO users
Access::service()?->commonName(); // "my-worker.access" for service tokens
```

That's it. Invalid, expired, or missing tokens are rejected before your route runs.

Configuration
-------------

[](#configuration)

### Accounts and applications

[](#accounts-and-applications)

Each **account** corresponds to one Cloudflare Zero Trust team (one team domain, one JWKS endpoint). Each **application** under it corresponds to a Cloudflare Access application with one or more AUD tags. Application names must be unique across all accounts, because middleware and guards refer to applications by name alone.

```
use ReturnEarly\CloudflareZeroTrust\Enums\PrincipalKind;

'accounts' => [
    'acme' => [
        // Your team domain. Also accepted under the key 'issuer'.
        'team_domain' => 'https://acme.cloudflareaccess.com',

        // Optional. Defaults to {team_domain}/cdn-cgi/access/certs
        // 'jwks_url' => 'https://acme.cloudflareaccess.com/cdn-cgi/access/certs',

        'applications' => [
            'internal-api' => [
                // One or more AUD tags. Also accepted under the key 'audiences'.
                'audience' => [env('CLOUDFLARE_INTERNAL_API_AUD')],

                // Optional. Prefer a per-app env var so you can disable enforcement
                // in local/staging without turning off every application.
                // When false, middleware passes through and the guard returns no user.
                'enabled' => (bool) env('CLOUDFLARE_INTERNAL_API_ENABLED', true),

                // Which principal kinds may authenticate.
                // Defaults to [PrincipalKind::User] — service tokens are opt-in.
                'principals' => [PrincipalKind::User, PrincipalKind::Service],

                // Optional. Set false to forbid identity enrichment for this app.
                'identity_enabled' => true,
            ],

            'horizon' => [
                'audience' => [env('CLOUDFLARE_HORIZON_AUD')],
                'enabled' => (bool) env('CLOUDFLARE_HORIZON_ENABLED', true),
                'principals' => [PrincipalKind::User], // humans only
            ],
        ],
    ],
],
```

Configuration is validated eagerly — a missing audience, an unknown principal kind, or a non-HTTPS team domain throws `InvalidConfiguration` rather than silently misbehaving.

### Package options

[](#package-options)

All top-level options with their environment variables and defaults:

Config keyEnv variableDefaultPurpose`enabled``CLOUDFLARE_ZERO_TRUST_ENABLED``true`Master switch. When `false`, middleware passes requests through unvalidated and the guard returns no user.`header`—`Cf-Access-Jwt-Assertion`Header containing the Access JWT. The `CF_Authorization` cookie is intentionally not read.`guard``CLOUDFLARE_ZERO_TRUST_GUARD``cloudflare`Default guard name, for your own reference in `config/auth.php`.`clock_leeway``CLOUDFLARE_ZERO_TRUST_CLOCK_LEEWAY``30`Seconds of clock skew tolerated when checking `exp`/`nbf`/`iat`.`jwks.cache_store``CLOUDFLARE_ZERO_TRUST_CACHE_STORE`default storeCache store for JWKS keys and identity payloads. Must support atomic locks.`jwks.cache_ttl``CLOUDFLARE_ZERO_TRUST_JWKS_CACHE_TTL``3600`Seconds to cache the JWKS key set.`jwks.connect_timeout``CLOUDFLARE_ZERO_TRUST_JWKS_CONNECT_TIMEOUT``2`JWKS HTTP connect timeout (seconds).`jwks.timeout``CLOUDFLARE_ZERO_TRUST_JWKS_TIMEOUT``5`JWKS HTTP request timeout (seconds).`jwks.refresh_rate_limit``CLOUDFLARE_ZERO_TRUST_JWKS_REFRESH_RATE_LIMIT``10`Max unknown-`kid` triggered refreshes per window.`jwks.refresh_rate_window``CLOUDFLARE_ZERO_TRUST_JWKS_REFRESH_RATE_WINDOW``60`Rate-limit window (seconds).`identity.enabled``CLOUDFLARE_ZERO_TRUST_IDENTITY_ENABLED``true`Global switch for identity enrichment.`identity.connect_timeout``CLOUDFLARE_ZERO_TRUST_IDENTITY_CONNECT_TIMEOUT``2`Identity HTTP connect timeout (seconds).`identity.timeout``CLOUDFLARE_ZERO_TRUST_IDENTITY_TIMEOUT``5`Identity HTTP request timeout (seconds).Protecting routes with middleware
---------------------------------

[](#protecting-routes-with-middleware)

The `cloudflare-access` middleware takes the application name as its parameter:

```
Route::middleware('cloudflare-access:internal-api')->group(function () {
    // API endpoints reachable by SSO users and service tokens
});

Route::middleware('cloudflare-access:horizon')->group(function () {
    // Human-only admin surface
});
```

To protect [Laravel Horizon](https://laravel.com/docs/horizon), reference the middleware in `config/horizon.php`:

```
'middleware' => ['web', 'cloudflare-access:horizon'],
```

### What a rejected request gets

[](#what-a-rejected-request-gets)

The middleware fails closed and never leaks *why* a token was rejected to the client (the detail goes into [events](#events) instead):

StatusWhenBody`401`Missing, malformed, expired, or otherwise invalid token`Unauthenticated.``403`Token is valid but the principal kind is not allowed for this application`Forbidden.``500`The middleware references an unknown application name`Cloudflare Access is misconfigured.``503`The Cloudflare JWKS endpoint is unreachable and no cached keys exist`Service Unavailable.`Requests that expect JSON receive `{"message": "..."}`; everything else gets a plain-text body.

Working with the verified principal
-----------------------------------

[](#working-with-the-verified-principal)

Once a request has been verified (by the middleware or the guard), the `Access` support class exposes the verified identity anywhere in your app:

```
use ReturnEarly\CloudflareZeroTrust\Support\Access;

Access::token();          // ?VerifiedAccessToken — raw JWT, header, claims, expiry
Access::principal();      // ?Principal — UserPrincipal or ServicePrincipal
Access::user();           // ?UserPrincipal — null if this is a service request
Access::service();        // ?ServicePrincipal — null if this is a user request
Access::isUser();         // bool
Access::isService();      // bool
Access::commonName();     // ?string — the service token's client ID
Access::applicationIs('horizon'); // bool — which named application authenticated this request
Access::identity();       // ?AccessIdentity — lazy enrichment; throws on failure
Access::tryIdentity();    // ?AccessIdentity — null instead of throwing
Access::hasGroup('Finance-Team'); // bool — never throws
```

All helpers return `null`/`false` on requests that were not verified, so they are safe to call unconditionally. State lives on the request instance (not the container), so it cannot bleed between requests under Octane; every helper also accepts an optional `Request` argument if you need to inspect a specific request.

### `UserPrincipal` (SSO users)

[](#userprincipal-sso-users)

A user token must carry `type=app`, a non-empty `sub`, and a valid `email` — otherwise it is rejected.

```
$user = Access::user();

$user->subject();       // stable Cloudflare user ID (JWT "sub")
$user->email();
$user->country();       // ?string, e.g. "US"
$user->accountName();   // config account name, e.g. "acme"
$user->applicationName();
$user->claims();        // full validated claim set
```

### `ServicePrincipal` (service tokens)

[](#serviceprincipal-service-tokens)

A service token must carry `type=app`, an *empty* `sub`, and a non-empty `common_name`.

```
$service = Access::service();

$service->commonName();  // the service token client ID, e.g. "my-worker.access"
$service->claims();
```

Both principal types implement Laravel's `Authenticatable` contract, so they also work with the auth guard below. `kind()` returns a `PrincipalKind` enum (`PrincipalKind::User` or `PrincipalKind::Service`).

Using the auth guard
--------------------

[](#using-the-auth-guard)

Register a guard with the `cloudflare` driver in `config/auth.php`:

```
'guards' => [
    'cloudflare' => [
        'driver' => 'cloudflare',
        'application' => 'internal-api',
    ],
],
```

Then authenticate the standard Laravel way:

```
Route::middleware('auth:cloudflare')->get('/me', function () {
    return auth('cloudflare')->user(); // UserPrincipal or ServicePrincipal
});

auth('cloudflare')->check();
auth('cloudflare')->id(); // user "sub" or service "common_name"
```

The guard and the middleware share one verification per request: whichever runs first stores the verified token on the request, and the other reuses it. The guard works standalone, but the package middleware gives you richer rejection behavior (403 vs 401, failure events), so stacking both is a good default:

```
Route::middleware(['cloudflare-access:internal-api', 'auth:cloudflare'])->group(/* ... */);
```

### Resolving Access users onto your own user model

[](#resolving-access-users-onto-your-own-user-model)

By default the guard returns the package's stateless principals. To map SSO users onto your application's `User` model, implement `ApplicationUserResolver`:

```
use Illuminate\Contracts\Auth\Authenticatable;
use ReturnEarly\CloudflareZeroTrust\Contracts\ApplicationUserResolver;
use ReturnEarly\CloudflareZeroTrust\Principals\UserPrincipal;

class CloudflareUserResolver implements ApplicationUserResolver
{
    public function resolve(UserPrincipal $principal): ?Authenticatable
    {
        return User::firstWhere('email', $principal->email());
    }
}
```

```
'guards' => [
    'cloudflare' => [
        'driver' => 'cloudflare',
        'application' => 'internal-api',
        'resolver' => App\Auth\CloudflareUserResolver::class,
    ],
],
```

A closure works too, if you configure the guard at runtime. Resolution rules:

- The resolver only runs for **user** principals; service principals are always returned as-is.
- If the resolver returns `null`, the guard falls back to the `UserPrincipal` (the request stays authenticated). Throw inside the resolver if an unknown SSO user should be treated as an error.

Service tokens
--------------

[](#service-tokens)

Machine-to-machine clients authenticate to Cloudflare with `CF-Access-Client-Id` / `CF-Access-Client-Secret` headers. **Cloudflare validates those at the edge** and forwards a signed JWT like any other request — this package only ever validates the resulting `Cf-Access-Jwt-Assertion` and never treats the client ID/secret pair as origin credentials.

Service tokens are **opt-in per application**. The default principal set is users only, so add `PrincipalKind::Service` where machines should be allowed:

```
use ReturnEarly\CloudflareZeroTrust\Enums\PrincipalKind;

'internal-api' => [
    'audience' => [env('CLOUDFLARE_INTERNAL_API_AUD')],
    'principals' => [PrincipalKind::User, PrincipalKind::Service],
],
```

An application without `PrincipalKind::Service` rejects service-token JWTs with `403` even when they are cryptographically valid. *Which* service tokens can mint a JWT for an application's audience is governed by your Cloudflare Access policy — manage that allowlist in the Cloudflare dashboard, not at the origin.

Identity enrichment
-------------------

[](#identity-enrichment)

The Access JWT deliberately carries a minimal claim set. When you need more — IdP groups, device posture, geo — the package fetches the full identity from your team domain's `/cdn-cgi/access/get-identity` endpoint:

```
$identity = Access::identity(); // ?AccessIdentity

$identity->email;
$identity->userUuid;
$identity->geo;            // ?string
$identity->ip;             // ?string
$identity->groups;         // list of IdP group names
$identity->idp;            // raw IdP payload
$identity->devicePosture;  // device posture checks
$identity->raw;            // the complete payload

Access::hasGroup('Finance-Team'); // convenience shortcut
```

Behavior to be aware of:

- **Lazy**: the endpoint is only called when you ask for the identity. Ordinary authentication never touches it.
- **Cached**: responses are cached (keyed by the token's identity nonce) for no longer than the JWT's remaining lifetime.
- **Users only**: identity lookups return `null` for service principals and on unverified requests.
- **Failure handling**: `Access::identity()` throws `IdentityUnavailable` if enrichment is disabled or the endpoint fails; use `Access::tryIdentity()` or `Access::hasGroup()` when you'd rather get `null`/`false`.
- **Can be disabled** globally (`CLOUDFLARE_ZERO_TRUST_IDENTITY_ENABLED=false`) or per application (`'identity_enabled' => false`).

Events
------

[](#events)

Every authentication outcome dispatches an event you can hook for logging, metrics, or alerting. No event ever contains the raw JWT.

EventWhenNotable payload`AccessAuthenticated`Middleware verified a token`account`, `application`, `principalKind` (enum), key ID`AccessAuthenticationFailed`Middleware rejected a request`reasonCode`, `account`, `application`, safe context`IdentityEnriched`Identity fetched from Cloudflare`account`, `application`, `subject``JwksRefreshed`JWKS key set fetched and cached`account`, `issuer`, `keyCount``JwksRefreshFailed`JWKS fetch failed`account`, `issuer`, `reason``reasonCode` values include `missing_token`, `malformed_token`, `invalid_algorithm`, `unknown_kid`, `invalid_signature_or_claims`, `invalid_issuer`, `invalid_audience`, `invalid_type`, `missing_sub`, `invalid_email`, `disallowed_principal`, and `jwks_unavailable` — enough granularity to alert on tampering separately from misconfiguration.

```
use ReturnEarly\CloudflareZeroTrust\Events\AccessAuthenticationFailed;

Event::listen(function (AccessAuthenticationFailed $event) {
    Log::warning('Cloudflare Access rejected a request', [
        'reason' => $event->reasonCode,
        'application' => $event->application,
    ]);
});
```

JWKS caching and key rotation
-----------------------------

[](#jwks-caching-and-key-rotation)

Cloudflare rotates your team's signing keys periodically. The package handles this without manual intervention:

- Keys are fetched from `{team_domain}/cdn-cgi/access/certs` and cached (default 1 hour, configurable store and TTL). The configured store must support atomic locks (Redis, Memcached, database, file, etc.).
- When a token arrives signed by an unknown `kid`, the key set is refreshed immediately — subject to a rate limit (default 10 refreshes per 60 seconds) so attackers can't use bogus `kid`s to hammer the JWKS endpoint.
- Concurrent refreshes are deduplicated with a cache lock on the same store.
- If a refresh fails but cached keys exist, the cached keys are used; if no keys are available at all, requests fail closed with `503`.
- Only RSA/RS256 signing keys are accepted; `alg` confusion (e.g. `none` or HS256 tokens) is rejected before signature verification.

Local development
-----------------

[](#local-development)

Validation is on by default everywhere. There is deliberately **no environment-name bypass** — an app that skips auth because `APP_ENV=local` is one typo away from skipping it in production. To work without Cloudflare in front of you, disable the package explicitly:

```
CLOUDFLARE_ZERO_TRUST_ENABLED=false
```

When disabled, the middleware passes requests through and the guard returns no user.

To disable a single application while keeping others enforced, set that application's `enabled` flag (via a per-app env var in config):

```
CLOUDFLARE_INTERNAL_API_ENABLED=false
```

Alternatively, run `cloudflared` locally and keep validation on — this exercises the real code path.

Testing your application
------------------------

[](#testing-your-application)

For feature tests that don't exercise authentication, disable the package in `phpunit.xml`:

```

```

To test the authentication path itself, fake the JWKS endpoint and mint your own RS256 tokens with `firebase/php-jwt` (already installed as a dependency of this package). Generate an RSA key pair in your test, serve its public JWK from the faked certs URL, and sign tokens with the private key:

```
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Http;

Http::fake([
    'https://acme.cloudflareaccess.com/cdn-cgi/access/certs' => Http::response([
        'keys' => [$publicJwk], // ['kty' => 'RSA', 'kid' => 'test-key', 'use' => 'sig', 'alg' => 'RS256', 'n' => ..., 'e' => ...]
    ]),
]);

$jwt = JWT::encode([
    'iss' => 'https://acme.cloudflareaccess.com',
    'aud' => ['your-aud-tag'],
    'type' => 'app',
    'sub' => 'user-uuid',
    'email' => 'user@example.com',
    'iat' => time(),
    'exp' => time() + 3600,
], $privateKeyPem, 'RS256', 'test-key');

$this->withHeader('Cf-Access-Jwt-Assertion', $jwt)
    ->getJson('/internal-api/reports')
    ->assertOk();
```

For a service-token request, replace `sub`/`email` with `'sub' => ''` and `'common_name' => 'my-client-id.access'`. See [this package's own test suite](tests/) for a complete key-pair fixture you can adapt.

Security model
--------------

[](#security-model)

What the package guarantees:

- Claims are trusted only after RS256 signature verification against your team's JWKS.
- Tokens are bound to the configured issuer and to the specific application's AUD tags — a token minted for one application cannot be replayed against another.
- Principal classification (user vs. service) is strict; ambiguous claim shapes are rejected.
- Verified auth state is request-scoped, so it cannot leak between requests under Octane or queue workers.
- Failures are closed: configuration errors, JWKS outages, and unknown applications reject requests rather than letting them through.
- Raw JWTs and secrets are never included in events, logs, or error responses.

What the package **cannot** do for you:

Important

Your origin must not be directly reachable from the internet. A valid Access JWT is a bearer credential: anyone who obtains one can replay it against an exposed origin until it expires, bypassing Cloudflare's checks entirely. Put your origin behind [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), mTLS, or equivalent network controls so the only path to your app is through Cloudflare.

See [SECURITY.md](SECURITY.md) for the full deployment threat model and the vulnerability disclosure policy.

Development
-----------

[](#development)

```
composer test      # Pest test suite
composer analyse   # PHPStan static analysis
composer format    # Laravel Pint code style
```

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md).

Credits
-------

[](#credits)

- [Tom Schlick](https://github.com/tomschlick)
- [All contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). See [LICENSE.md](LICENSE.md) for details.

###  Health Score

21

—

LowBetter than 17% of packages

Maintenance65

Regular maintenance activity

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

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.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/70184?v=4)[Tom Schlick](/maintainers/tomschlick)[@tomschlick](https://github.com/tomschlick)

---

Top Contributors

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

### Embed Badge

![Health badge](/badges/returnearly-laravel-cloudflare-zero-trust/health.svg)

```
[![Health](https://phpackages.com/badges/returnearly-laravel-cloudflare-zero-trust/health.svg)](https://phpackages.com/packages/returnearly-laravel-cloudflare-zero-trust)
```

PHPackages © 2026

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