masterix21/laravel-licensing-client
Laravel Licensing Client
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.
Credits
Related Packages
Laravel licensing package with polymorphic assignment to any model, activation k...
Simple and flexible entitlement management for Laravel applications, with suppor...
This is my package laravel-licensing-filament-manager
Powerful PHP database abstraction layer (DBAL) with many features for database s...
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| 2.0.0 | ^8.3 | | ^12.0| | MIT | |
| v1.0.0 | ^8.2 | ^12.0 | MIT |