masterix21 / laravel-licensing-client
This is my package laravel-licensing-client
Package info
github.com/masterix21/laravel-licensing-client
pkg:composer/masterix21/laravel-licensing-client
Fund package maintenance!
Requires
- php: ^8.3 || ^8.4 || ^8.5
- guzzlehttp/guzzle: ^7.8
- illuminate/contracts: ^12.0||^13.0
- illuminate/http: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
- paragonie/paseto: ^3.2
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- masterix21/laravel-licensing: dev-main
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.6
- orchestra/testbench-core: ^10.6.5
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-04-20 20:58:39 UTC
README
A Laravel package for integrating license validation in your applications. Works with the Laravel Licensing server to provide secure, offline-capable license management using PASETO v4 tokens with Ed25519 signatures.
Related Packages
- Laravel Licensing Server - Server-side license management
- Laravel Licensing Filament Manager - Admin panel built with Filament
Requirements
- PHP 8.3+
- Laravel 12 or 13
Installation
composer require masterix21/laravel-licensing-client
Publish the configuration:
php artisan vendor:publish --tag="licensing-client-config"
Run the migrations:
php artisan migrate
Configuration
Add these variables to your .env:
LICENSING_SERVER_URL=https://your-licensing-server.com LICENSING_PUBLIC_KEY=your-base64-encoded-ed25519-public-key LICENSING_KEY=LIC-XXXX-XXXX-XXXX-XXXX
The full configuration is in config/licensing-client.php:
return [ 'server_url' => env('LICENSING_SERVER_URL', 'https://licensing.example.com'), 'api_version' => env('LICENSING_API_VERSION', 'v1'), 'license_key' => env('LICENSING_KEY'), 'public_key' => env('LICENSING_PUBLIC_KEY'), 'issuer' => env('LICENSING_ISSUER', 'laravel-licensing'), 'cache' => [ 'enabled' => env('LICENSING_CACHE_ENABLED', true), 'store' => env('LICENSING_CACHE_STORE', 'file'), 'ttl' => env('LICENSING_CACHE_TTL', 3600), ], 'heartbeat' => [ 'enabled' => env('LICENSING_HEARTBEAT_ENABLED', true), 'interval' => env('LICENSING_HEARTBEAT_INTERVAL', 3600), ], 'grace_period_days' => env('LICENSING_GRACE_PERIOD_DAYS', 7), 'timeout' => env('LICENSING_TIMEOUT', 30), 'debug' => env('LICENSING_DEBUG', false), 'storage_path' => storage_path('app/licensing'), 'excluded_routes' => [ 'login', 'register', 'password/*', 'licensing/*', ], ];
Usage
Facade
use LucaLongo\LaravelLicensingClient\Facades\LaravelLicensingClient; // Activate LaravelLicensingClient::activate('LIC-XXXX-XXXX-XXXX-XXXX'); // Check validity (offline, from stored PASETO token) LaravelLicensingClient::isValid(); // Validate with exception on failure $claims = LaravelLicensingClient::validate(); // Get license info from token claims $info = LaravelLicensingClient::getLicenseInfo(); // Returns: [ // 'license_id' => 1, // 'license_key_hash' => 'sha256...', // 'status' => 'active', // 'max_usages' => 5, // 'expires_at' => '2027-01-07T00:00:00+00:00', // 'issued_at' => '2027-01-01T00:00:00+00:00', // 'license_expires_at' => '2027-12-31T23:59:59+00:00', // 'force_online_after' => '2027-01-14T00:00:00+00:00', // 'grace_until' => null, // 'usage_fingerprint' => 'sha256...', // ] // Check expiration warnings LaravelLicensingClient::isExpiringSoon(7); // Check if online refresh is required (force_online_after exceeded) LaravelLicensingClient::requiresOnlineRefresh(); // Proactive refresh check (based on refresh_after from server) LaravelLicensingClient::shouldRefreshProactively(); // Refresh the token LaravelLicensingClient::refresh(); // Deactivate (with optional reason) LaravelLicensingClient::deactivate('LIC-XXXX-XXXX-XXXX-XXXX', 'switching device'); // Server health LaravelLicensingClient::isServerHealthy();
Dependency Injection
use LucaLongo\LaravelLicensingClient\LaravelLicensingClient; class LicenseController extends Controller { public function status(LaravelLicensingClient $licensing) { return response()->json([ 'valid' => $licensing->isValid(), 'info' => $licensing->getLicenseInfo(), 'requires_refresh' => $licensing->requiresOnlineRefresh(), ]); } }
Middleware
Protect routes with the license middleware:
// Single route Route::get('/dashboard', DashboardController::class)->middleware('license'); // Route group Route::middleware('license')->group(function () { Route::get('/reports', ReportsController::class); Route::get('/analytics', AnalyticsController::class); });
Middleware Behavior
The middleware follows this flow:
- Check if the route is excluded
- Validate the stored token offline
- If valid, check
force_online_after— refresh if past date - If token invalid, attempt refresh from server
- If refresh fails, check client-side grace period
- If not in grace period, check server health
- If server unreachable, start grace period and allow access
- If server healthy and no valid license, block with 403
On valid requests, the middleware also:
- Sends heartbeat if interval has elapsed
- Sets
license_expiring_soonandlicense_expires_atas request attributes if expiration is near
Accessing Expiration Warnings
public function dashboard(Request $request) { if ($request->attributes->get('license_expiring_soon')) { $expiresAt = $request->attributes->get('license_expires_at'); // Show renewal warning } }
Excluding Routes
Configure in config/licensing-client.php:
'excluded_routes' => [ 'login', 'register', 'password/*', 'api/health', ],
Grace Period
The client manages a local grace period when the licensing server is unreachable:
// Check if in grace period LaravelLicensingClient::isInGracePeriod(); // Manually start (useful for testing) LaravelLicensingClient::startGracePeriod();
The default grace period is 7 days, configurable via LICENSING_GRACE_PERIOD_DAYS.
The middleware automatically enters grace period when the server is unreachable, allowing the application to continue working.
Artisan Commands
# Activate a license (interactive if no key provided) php artisan license:activate LIC-XXXX-XXXX-XXXX-XXXX # Validate current license php artisan license:validate # Display license details php artisan license:info # Refresh token from server php artisan license:refresh # Deactivate license (with confirmation prompt) php artisan license:deactivate
Heartbeat
When enabled, the package automatically sends heartbeats to the licensing server at the configured interval. The heartbeat reports:
- Laravel version
- Application environment
Configure in .env:
LICENSING_HEARTBEAT_ENABLED=true LICENSING_HEARTBEAT_INTERVAL=3600 # seconds
The heartbeat is registered as a scheduled task in the service provider and runs via Laravel's scheduler.
Token Validation
The client validates PASETO v4 tokens offline using the Ed25519 public key. The following claims are validated:
| Claim | Validation |
|---|---|
usage_fingerprint |
Must match the current device fingerprint |
exp |
Token must not be expired |
status |
Must be active or grace |
force_online_after |
If past, an online refresh is required |
The client also stores the public_key_bundle received from the server during activation and refresh, enabling future key rotation support.
Device Fingerprinting
The client generates a stable SHA-256 fingerprint from:
- Hostname
- Machine ID (platform-specific:
/etc/machine-id,IOPlatformUUID, WMI UUID) - PHP version
- Laravel version
- Application key
This fingerprint is sent to the server during activation to bind the license to the device.
Custom Fingerprint Generator
use LucaLongo\LaravelLicensingClient\Services\FingerprintGenerator; class CustomFingerprintGenerator extends FingerprintGenerator { public function generate(): string { $components = [ $this->getHostname(), $this->getMachineId(), config('app.deployment_id'), ]; return hash('sha256', implode('|', array_filter($components))); } } // Register in a service provider $this->app->bind(FingerprintGenerator::class, CustomFingerprintGenerator::class);
Error Handling
The package throws LicensingException with specific factory methods:
use LucaLongo\LaravelLicensingClient\Exceptions\LicensingException; try { LaravelLicensingClient::validate(); } catch (LicensingException $e) { // Possible messages: // - "The provided license key is invalid." // - "The license has expired." // - "The license has not been activated." // - "The license has been suspended." // - "The license has been cancelled." // - "Device fingerprint does not match the licensed device." // - "The fingerprint is already in use by another device." // - "License usage limit has been exceeded." // - "Offline tokens are not enabled for this license." // - "Too many requests to the licensing server. Please try again later." // - "Online verification is required. Please connect to the internet." // - "Unable to reach the licensing server." // - "The license token is invalid or corrupted." // - "Public key for token verification is not configured." }
API Communication
The client communicates with the server at /api/licensing/v1/ using these endpoints:
| Method | Endpoint | Description |
|---|---|---|
| POST | /activate |
Activate a license with fingerprint |
| POST | /deactivate |
Deactivate a license |
| POST | /refresh |
Refresh the PASETO token |
| POST | /heartbeat |
Send heartbeat with usage data |
| POST | /validate |
Validate license server-side |
| POST | /licenses/show |
Get license information |
| GET | /health |
Check server health |
All responses follow the format:
{
"success": true,
"data": { ... }
}
Error responses:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message"
}
}
The client handles these HTTP error codes: 404 (invalid key), 403 (fingerprint mismatch / not active), 409 (usage limit / fingerprint conflict / offline disabled), 410 (expired), 422 (validation failed), 423 (suspended / cancelled), 429 (rate limited).
Testing
In Your Application Tests
use Illuminate\Support\Facades\Http; use LucaLongo\LaravelLicensingClient\Facades\LaravelLicensingClient; public function test_protected_route(): void { Http::fake([ '*/api/licensing/v1/activate' => Http::response([ 'success' => true, 'data' => [ 'token' => 'v4.public.test-token...', 'license' => ['id' => 'ulid', 'status' => 'active'], ], ]), ]); LaravelLicensingClient::activate('TEST-KEY'); $this->get('/protected-route')->assertStatus(200); }
Mocking the Client
use LucaLongo\LaravelLicensingClient\LaravelLicensingClient; $mock = Mockery::mock(LaravelLicensingClient::class); $mock->shouldReceive('isValid')->andReturn(true); $mock->shouldReceive('getLicenseInfo')->andReturn([ 'status' => 'active', 'max_usages' => 5, ]); $this->app->instance(LaravelLicensingClient::class, $mock);
Running Package Tests
composer test # Run all tests composer test-coverage # Run with coverage composer analyse # PHPStan static analysis composer format # Laravel Pint formatting
Development Roadmap
The following features are planned for future releases:
Phase 1 — Token Security
iss(issuer) claim validationnbf(not before) claim validation- Clock skew tolerance (configurable, default ±60s)
Phase 2 — Features & Entitlements
hasFeature(string $feature): boolandgetFeatures(): arraygetEntitlement(string $key, mixed $default = null): mixed- Feature-gating middleware:
Route::middleware('license:premium_export') - Features and entitlements are stored from the API response (not in the token)
Phase 3 — Network Resilience
- Automatic retry with exponential backoff via
Http::retry()
Phase 4 — License Information
- Seat info:
getSeatsInfo()(active/available/max usages) - License vs token expiry distinction:
isLicenseExpiringSoon() - Server-side grace period awareness from
grace_untiltoken claim
Phase 5 — Key Rotation
- Public key selection via
kidfrom token footer - Proactive scheduled token refresh based on
refresh_after
Contributing
Contributions are welcome! Please submit a Pull Request.
License
MIT License. See LICENSE.md.