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.
Fund package maintenance!
Luca Longo
Requires
- php: ^8.3
- ext-openssl: *
- ext-sodium: *
- illuminate/console: ^12.0
- illuminate/contracts: ^12.0
- illuminate/database: ^12.0
- illuminate/http: ^12.0
- illuminate/support: ^12.0
- paragonie/paseto: ^3.0
- spatie/crypto: ^2.0
- spatie/laravel-package-tools: ^1.16
- spatie/laravel-sluggable: ^3.7
- symfony/uid: ^7.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.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
- spatie/pest-plugin-test-time: ^2.2
README
Enterprise-grade license management for Laravel applications with offline verification, seat-based licensing, cryptographic security, and multi-product support through License Scopes.
Installation
Install the package via Composer:
composer require masterix21/laravel-licensing
Publish the configuration and migrations:
php artisan vendor:publish --provider="LucaLongo\Licensing\LicensingServiceProvider"
Run the migrations:
php artisan migrate
Generate your root certificate authority key:
php artisan licensing:keys:make-root
Passphrase required: The command encrypts keys using a passphrase stored in the
LICENSING_KEY_PASSPHRASE
environment variable (configurable vialicensing.crypto.keystore.passphrase_env
). If the variable is missing, the command will now prompt you to create one unless you run it with--silent
/--no-interaction
. Set it ahead of time (for exampleexport LICENSING_KEY_PASSPHRASE="your-strong-passphrase"
) to enable non-interactive automation.
Issue your first signing key:
php artisan licensing:keys:issue-signing --kid signing-key-1
Key Features
- ๐ Offline Verification: PASETO v4 tokens with Ed25519 signatures
- ๐ช Seat-Based Licensing: Control device/user limits per license
- ๐ License Lifecycles: Activation, renewal, grace periods, and expiration
- ๐ข Multi-Product Support: License Scopes for product/software isolation
- ๐ Two-Level Key Hierarchy: Root CA โ Signing Keys for secure rotation
- ๐ Comprehensive Audit Trail: Track all license and usage events
- ๐ฏ Flexible Assignment: Polymorphic relationships for any model
- ๐พ Flexible Key Management: Auto-generation, custom keys, optional retrieval
- ๐ Secure Storage: Encrypted key storage with configurable retrieval
- โก High Performance: Optimized for enterprise workloads
Quick Start
1. Create and activate a license
use LucaLongo\Licensing\Models\License; use LucaLongo\Licensing\Models\LicenseScope; // Method 1: Auto-generate license key $license = License::createWithKey([ 'licensable_type' => User::class, 'licensable_id' => $user->id, 'max_usages' => 5, 'expires_at' => now()->addYear(), ]); // The generated key is available immediately after creation $licenseKey = $license->license_key; // e.g., "LIC-A3F2-B9K1-C4D8-E5H7" // Method 2: Provide your own license key $customKey = 'CUSTOM-KEY-12345'; $license = License::createWithKey([ 'licensable_type' => User::class, 'licensable_id' => $user->id, 'max_usages' => 5, 'expires_at' => now()->addYear(), ], $customKey); // Method 3: Traditional approach with hash only $activationKey = Str::random(32); $license = License::create([ 'key_hash' => License::hashKey($activationKey), 'licensable_type' => User::class, 'licensable_id' => $user->id, 'license_scope_id' => $scope->id ?? null, // Optional scope 'max_usages' => 5, 'expires_at' => now()->addYear(), ]); $license->activate();
2. Register a device
use LucaLongo\Licensing\Facades\Licensing; $usage = Licensing::register( $license, 'device-fingerprint-hash', ['device_name' => 'MacBook Pro'] );
3. Issue an offline token
$token = Licensing::issueToken($license, $usage, [ 'ttl_days' => 7, ]);
4. Verify license
if ($license->isUsable()) { $remainingDays = $license->daysUntilExpiration(); $availableSeats = $license->getAvailableSeats(); }
5. Retrieve and manage license keys
// Retrieve the original license key (if stored encrypted) $originalKey = $license->retrieveKey(); // Check if retrieval is available if ($license->canRetrieveKey()) { $key = $license->retrieveKey(); } // Regenerate a license key if ($license->canRegenerateKey()) { $newKey = $license->regenerateKey(); // Old key no longer works, new key is returned } // Verify a license key $isValid = $license->verifyKey($providedKey); // Find license by key $license = License::findByKey($licenseKey);
License Key Management
The package provides flexible license key management with three configurable services:
Configuration
// config/licensing.php 'services' => [ 'key_generator' => \LucaLongo\Licensing\Services\EncryptedLicenseKeyGenerator::class, 'key_retriever' => \LucaLongo\Licensing\Services\EncryptedLicenseKeyRetriever::class, 'key_regenerator' => \LucaLongo\Licensing\Services\EncryptedLicenseKeyRegenerator::class, ], 'key_management' => [ 'retrieval_enabled' => true, // Allow retrieving original keys 'regeneration_enabled' => true, // Allow regenerating keys 'key_prefix' => 'LIC', // Prefix for generated keys 'key_separator' => '-', // Separator for key segments ],
Custom Key Services
You can implement your own key management services:
use LucaLongo\Licensing\Contracts\LicenseKeyGeneratorContract; class CustomKeyGenerator implements LicenseKeyGeneratorContract { public function generate(?License $license = null): string { // Your custom key generation logic return 'CUSTOM-' . strtoupper(bin2hex(random_bytes(8))); } }
Then register it in the config:
'services' => [ 'key_generator' => \App\Services\CustomKeyGenerator::class, ],
Security Considerations
- Hashed Storage: Keys are always stored as salted SHA-256 hashes
- Encrypted Retrieval: Original keys can be stored encrypted (optional)
- Regeneration History: Previous key hashes are maintained for audit
- Configurable: Disable retrieval/regeneration for maximum security
Multi-Product Licensing with Scopes
License Scopes enable you to manage multiple products/software with isolated signing keys, preventing key compromise in one product from affecting others.
Create product scopes
use LucaLongo\Licensing\Models\LicenseScope; // Create scope for your ERP system $erpScope = LicenseScope::create([ 'name' => 'ERP System', 'slug' => 'erp-system', 'identifier' => 'com.company.erp', 'key_rotation_days' => 90, 'default_max_usages' => 100, ]); // Create scope for your mobile app $mobileScope = LicenseScope::create([ 'name' => 'Mobile App', 'slug' => 'mobile-app', 'identifier' => 'com.company.mobile', 'key_rotation_days' => 30, // More frequent rotation 'default_max_usages' => 3, ]);
Issue scoped signing keys
# Issue signing key for ERP system php artisan licensing:keys:issue-signing --scope erp-system --kid erp-key-2024 # Issue signing key for mobile app php artisan licensing:keys:issue-signing --scope mobile-app --kid mobile-key-2024
Create scoped licenses
// Create license for ERP system $erpLicense = License::create([ 'key_hash' => License::hashKey($erpActivationKey), 'license_scope_id' => $erpScope->id, // Scoped to ERP 'licensable_type' => Company::class, 'licensable_id' => $company->id, 'max_usages' => 100, 'expires_at' => now()->addYear(), ]); // Create license for mobile app $mobileLicense = License::create([ 'key_hash' => License::hashKey($mobileActivationKey), 'license_scope_id' => $mobileScope->id, // Scoped to mobile 'licensable_type' => User::class, 'licensable_id' => $user->id, 'max_usages' => 3, 'expires_at' => now()->addMonths(6), ]); // Tokens are automatically signed with the correct scope-specific key $erpToken = Licensing::issueToken($erpLicense, $erpUsage); $mobileToken = Licensing::issueToken($mobileLicense, $mobileUsage);
Benefits of License Scopes
- Key Isolation: Each product has its own signing keys
- Independent Rotation: Different rotation schedules per product
- Blast Radius Limitation: Key compromise affects only one product
- Product-Specific Defaults: Configure max usages, trial days per scope
- Flexible Management: Programmatic or CLI-based key management
Testing
Run the test suite:
composer test
Run tests with coverage:
composer test-coverage
Static analysis:
composer analyse
Documentation
For comprehensive documentation visit the documentation.
AI Assistant Support
This package includes comprehensive guidelines for AI coding assistants. See AI_GUIDELINES.md for:
- Claude Code integration patterns
- ChatGPT/Codex usage examples
- GitHub Copilot autocomplete triggers
- Junie configuration and patterns
Requirements
- PHP 8.3+
- Laravel 12.0+
- OpenSSL extension
- Sodium extension (for PASETO tokens and Ed25519 signatures)
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
Security
If you discover any security-related issues, please email security@example.com instead of using the issue tracker. All security vulnerabilities will be promptly addressed.
License
The MIT License (MIT). Please see License File for more information.