masterix21/laravel-licensing-client
Laravel client for integrating license validation into your app. Verify keys, activate or deactivate installations, and check license status against a remote licensing server, with a simple, configurable API for gating features and managing subscriptions.
Install and publish config
composer require masterix21/laravel-licensing-client:^2.0
php artisan vendor:publish --tag="licensing-client-config"
Confirm your platform requirements first: 2.0 requires a modern PHP runtime (8.4/8.5 are explicitly supported and tested, alongside Laravel 13 in CI). If you are still on PHP 8.1/8.2 or Laravel 10, stay on 1.x — check composer.json constraints before bumping.
Set the issuer before anything else
The single most important step after upgrading is configuring the issuer so it matches the value your laravel-licensing server stamps into the iss claim:
// config/licensing-client.php
'issuer' => env('LICENSING_ISSUER', 'https://licensing.example.com'),
A mismatch is not a warning — every token is rejected. Set this in .env per environment (staging and production servers usually issue different iss values) and re-run php artisan config:cache if you cache config.
Bootstrap the public key bundle
The client verifies PASETO tokens offline against an Ed25519 public key. The validator can be initialised from a previously stored bundle on boot, and the bundle is refreshed from server responses. The practical first-run flow is:
First use case: gate a feature by entitlement
The most common day-to-day use is answering "is this install/tenant allowed to use feature X?" Read entitlements from the validated token rather than calling the server:
$token = Licensing::validate($storedToken); // offline, Ed25519-verified
if ($token->entitlements()->contains('reports.export')) {
// allow
}
Start by wiring one low-risk feature behind an entitlement check and confirming the happy path plus the "token expired" path before rolling it out broadly.
Middleware for licensed routes
Wrap premium route groups in a middleware that validates the stored token and aborts with a 402/403 when the license is missing, expired, or lacks the entitlement. Keep the middleware thin — validation result plus entitlement check — and let the controller deal with the feature itself. This gives you a single, auditable choke point instead of scattered if (Licensing::check()) calls.
Gate/ability mapping to entitlements
Expose entitlements as Laravel gates or abilities so the rest of the app speaks idiomatic Laravel:
Gate::define('export-reports', fn ($user) =>
$user->currentTeam->licenseToken()?->entitlements()->contains('reports.export') ?? false
);
Then use @can('export-reports') in Blade and $this->authorize(...) in controllers. The entitlements API is the natural seam between "licensing" and "authorization" — keep it there and the licensing package stays isolated to one or two files.
Polymorphic licensable models
The token now exposes licensable_type and licensable_id. Use these to bind a token to the model that owns it (a Team, Installation, Tenant, etc.) and to reject tokens being replayed against the wrong subject. A common pattern is a HasLicense trait that stores the token and compares licensable_type/licensable_id on retrieval:
abort_unless(
$token->licensable_type === $this->getMorphClass()
&& (string) $token->licensable_id === (string) $this->getKey(),
403
);
Scheduled re-validation (heartbeat) instead of per-request calls
Because verification is offline, you do not want an HTTP call on every request. Run a scheduled command (hourly or daily) that fetches the current token/bundle from the server, applies the new bundle to the validator, and persists both. Between runs, requests validate against the stored token locally. This is the pattern that makes the package cheap at runtime and is the whole point of the certificate-chain work in 2.0.
Graceful degradation and revocation lag
Offline verification means a revoked license keeps working until the next heartbeat. Design for that: keep the heartbeat interval short enough for your commercial tolerance, and have a license:refresh command you can trigger from an admin action ("re-sync license now"). On heartbeat failures, distinguish network errors from license errors — see the HTTP mapping notes below.
Queue workers and long-lived processes
The validator is initialised from the stored bundle on boot. Long-running queue workers (Horizon, Octane) will keep the validator they booted with. After a key rotation or bundle refresh, restart workers (php artisan queue:restart, octane:reload) or explicitly re-initialise the validator inside the job. Wrapping validation in a small service that checks a "bundle version" cache key before trusting the in-memory validator is a robust way to handle this.
Testing
Fake the client in feature tests so CI never depends on a licensing server, and keep one or two integration tests against a real/stubbed server for the token round-trip. Use Carbon::setTestNow() to exercise expiration and not-before behaviour deterministically — with a ±60s skew this matters.
The issuer config is the #1 upgrade trap
Tokens with an invalid iss are now rejected outright. If you upgrade without setting licensing-client.issuer, every previously working token starts failing at once. Symptom: valid-looking tokens rejected immediately after composer update. Fix the config, then php artisan config:cache — forgetting the cache refresh in production is the classic second-order failure.
Certificate chain verification can invalidate "good" tokens
Tokens carrying a certificate chain in the footer are now verified against the root public key. If your stored root key predates a server-side rotation, or your stored bundle is stale, validation fails even though the token is genuinely from your server. Recovery path: force a bundle refresh from the server (heartbeat) rather than hand-editing stored keys. Also verify that your token storage persists the bundle, not just the token — a token alone is no longer sufficient context after a rotation.
Clock skew is your friend and your debugging enemy
Expiration and nbf checks now tolerate ±60s, configurable. Two practical consequences:
nbf validation changes activation timing
If you mint a token and immediately validate it in the same request, and the issuing server's clock is even slightly ahead, you can hit a not-before rejection. The default skew handles most of this, but be aware when writing tests or doing synchronous activate-then-validate flows.
Don't deactivate on 5xx
HTTP error mapping is improved in 2.0 (400 and 5xx are now differentiated). Treat 400-class responses as "this license/token request is wrong" (bad key, wrong issuer, malformed payload) and 5xx as "the server is having a bad day". Only the former should ever lead to marking a license invalid; a 5xx during a heartbeat must fall back to the last known good token and bundle. Getting this backwards leads to paying customers losing access during your vendor's outage.
Laravel 13 / PHP 8.5 in the test matrix ≠ requirement
CI testing Laravel 13 means compatibility, not that you must upgrade. Pin your constraint to what you actually run and let the matrix inform your upgrade planning rather than forcing it.
Server compatibility is a two-sided upgrade
Full compatibility is with laravel-licensing server v2.0.0. Upgrading the client while the server is still on 1.x (or vice versa) is the most likely source of confusing signature/issuer failures. Upgrade them together, or at least verify the issuer and bundle endpoints behave as the client expects.
Extension points worth knowing
The stored bundle on boot and the "apply bundle from response" hook are the two seams you will most likely customise: wrapping bundle persistence (e.g. encrypted at rest, or keyed per tenant) and injecting a validator factory are the standard moves when you need multi-tenant licensing or non-default storage. Keep those in a dedicated service provider so package upgrades stay painless.
Log the claims, not the token
When debugging, log issuer, not_before, licensable_type/licensable_id, and the entitlement names — never the raw PASETO or key material. These four fields resolve the overwhelming majority of "license not working" tickets: wrong issuer, clock/nbf, wrong subject binding, or missing entitlement.
How can I help you explore Laravel packages today?