KNM.LicenseValidator 1.1.5
KNM License Validator
A robust, enterprise-grade license validation library for .NET 10 applications with hybrid offline/online validation, triple-layer security, and fluent configuration API.
โจ Features
- ๐ Hybrid Validation: Online-first with automatic offline fallback for maximum reliability
- ๐ Triple Security Layer: RSA signatures + AES-256 encryption + HMAC integrity verification
- โก Lightweight & Fast: Zero database dependencies - integrate with your existing data layer
- ๐ฏ Fluent Configuration: Modern builder pattern with
.With()methods for intuitive setup - ๐ Custom Storage Providers: Implement
ILicenseKeyProviderfor database, Azure Key Vault, or any storage - ๐ Custom License Format: Branded license files with Base32 encoding
- ๐ง AOT & Trimming Ready: Fully compatible with Native AOT compilation
- ๐ฆ Dependency Injection: First-class DI support with ASP.NET Core integration
- ๐พ Intelligent Caching: Built-in validation result caching with configurable expiration
- ๐ Startup Validation: Optional background service for license validation on application startup
๐ Requirements
- .NET 10.0
- C# 13 or later
- KNM.CryptoHelper (automatically installed)
๐ฆ Installation
From Private NuGet Feed
# Install the package
dotnet add package KNM.LicenseValidator
From Local Path
dotnet add package KNM.LicenseValidator --source "./local-packages"
PackageReference
<PackageReference Include="KNM.LicenseValidator" Version="1.1.5" />
๐ Quick Start
1. Minimal Configuration
using KNM.LicenseValidator.Extensions;
using KNM.LicenseValidator.Configuration;
var builder = WebApplication.CreateBuilder(args);
// Simple offline validation
builder.Services.AddKnmLicenseValidator(config =>
{
config.Mode = ValidationMode.OfflineOnly;
config.LicenseDirectory = "./Licenses";
});
var app = builder.Build();
app.Run();
2. Fluent Configuration (Recommended)
builder.Services.AddKnmLicenseValidator(validator =>
{
validator
.WithValidationMode(ValidationMode.Hybrid)
.WithApiSettings(
baseUrl: "https://api.yourcompany.com",
apiKey: "your-api-key",
timeout: TimeSpan.FromSeconds(60))
.WithOfflineFiles(
licenseDirectory: "./Licenses",
publicKeyFileName: "PublicKey.xml",
licenseFileName: "License.txt")
.WithCacheExpiration(TimeSpan.FromHours(24))
.WithFallbackBehavior(
fallbackToOffline: true,
fallbackToOnline: false)
.WithSecuritySettings(
strictValidation: true,
logAttempts: true)
.WithStartupValidation();
});
3. Configuration from appsettings.json
appsettings.json:
{
"LicenseValidator": {
"Mode": "Hybrid",
"ApiBaseUrl": "https://api.yourcompany.com",
"ApiKey": "your-api-key-here",
"LicenseDirectory": "./Licenses",
"PublicKeyFileName": "PublicKey.xml",
"LicenseFileName": "License.txt",
"CacheExpiration": "1.00:00:00",
"ApiTimeout": "00:00:30",
"ValidateOnStartup": true,
"FallbackToOffline": true,
"StrictValidation": true,
"SoftwareId": 1
}
}
Program.cs:
// Load from configuration
builder.Services.AddKnmLicenseValidator(builder.Configuration);
// Or with custom provider
builder.Services.AddKnmLicenseValidator(validator =>
{
validator
.UseCustomProvider<DatabaseLicenseKeyProvider>()
.WithStartupValidation();
});
4. Basic Usage
public class LicenseService
{
private readonly ILicenseValidator _validator;
private readonly ILogger<LicenseService> _logger;
public LicenseService(
ILicenseValidator validator,
ILogger<LicenseService> logger)
{
_validator = validator;
_logger = logger;
}
public async Task<bool> CheckLicenseAsync()
{
var result = await _validator.ValidateLicenseAsync();
if (result.IsValid)
{
_logger.LogInformation(
"License valid until {ExpirationDate} (Source: {Source})",
result.LicenseInfo?.ExpirationDate,
result.Source
);
return true;
}
_logger.LogWarning("License validation failed: {Error}", result.ErrorMessage);
return false;
}
public async Task<bool> CheckSpecificFeatureAsync(string featureName)
{
var result = await _validator.ValidateLicenseAsync();
return result.HasFeature(featureName, "features");
}
}
๐ Advanced Usage
Custom License Key Provider
The library provides flexible storage options through the ILicenseKeyProvider interface. Implement it for custom storage solutions like databases, Azure Key Vault, distributed caches, or any other storage mechanism.
ILicenseKeyProvider Interface
public interface ILicenseKeyProvider
{
/// <summary>
/// Retrieves the license key from storage
/// </summary>
Task<string?> GetLicenseKeyAsync();
/// <summary>
/// Stores a license key in the storage
/// </summary>
Task<bool> StoreLicenseKeyAsync(string licenseKey);
/// <summary>
/// Removes the license key from storage
/// </summary>
Task<bool> ClearLicenseKeyAsync();
}
Example: Database Provider
public class DatabaseLicenseKeyProvider : ILicenseKeyProvider
{
private readonly ApplicationDbContext _context;
private readonly ILogger<DatabaseLicenseKeyProvider> _logger;
public DatabaseLicenseKeyProvider(
ApplicationDbContext context,
ILogger<DatabaseLicenseKeyProvider> logger)
{
_context = context;
_logger = logger;
}
public async Task<string?> GetLicenseKeyAsync()
{
try
{
var license = await _context.Licenses
.Where(l => l.IsActive)
.OrderByDescending(l => l.CreatedAt)
.FirstOrDefaultAsync();
_logger.LogDebug("Retrieved license from database");
return license?.LicenseKey;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve license from database");
return null;
}
}
public async Task<bool> StoreLicenseKeyAsync(string licenseKey)
{
try
{
// Deactivate old licenses
var oldLicenses = await _context.Licenses
.Where(l => l.IsActive)
.ToListAsync();
foreach (var old in oldLicenses)
{
old.IsActive = false;
}
// Add new license
var license = new License
{
LicenseKey = licenseKey,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
_context.Licenses.Add(license);
await _context.SaveChangesAsync();
_logger.LogInformation("License stored successfully in database");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to store license in database");
return false;
}
}
public async Task<bool> ClearLicenseKeyAsync()
{
try
{
var licenses = await _context.Licenses.ToListAsync();
_context.Licenses.RemoveRange(licenses);
await _context.SaveChangesAsync();
_logger.LogInformation("All licenses cleared from database");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clear licenses from database");
return false;
}
}
}
Example: Azure Key Vault Provider
public class AzureKeyVaultLicenseProvider : ILicenseKeyProvider
{
private readonly SecretClient _secretClient;
private readonly ILogger<AzureKeyVaultLicenseProvider> _logger;
private const string SecretName = "LicenseKey";
public AzureKeyVaultLicenseProvider(
SecretClient secretClient,
ILogger<AzureKeyVaultLicenseProvider> logger)
{
_secretClient = secretClient;
_logger = logger;
}
public async Task<string?> GetLicenseKeyAsync()
{
try
{
var secret = await _secretClient.GetSecretAsync(SecretName);
_logger.LogDebug("Retrieved license from Azure Key Vault");
return secret.Value.Value;
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
_logger.LogWarning("License not found in Azure Key Vault");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve license from Azure Key Vault");
return null;
}
}
public async Task<bool> StoreLicenseKeyAsync(string licenseKey)
{
try
{
await _secretClient.SetSecretAsync(SecretName, licenseKey);
_logger.LogInformation("License stored successfully in Azure Key Vault");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to store license in Azure Key Vault");
return false;
}
}
public async Task<bool> ClearLicenseKeyAsync()
{
try
{
var operation = await _secretClient.StartDeleteSecretAsync(SecretName);
await operation.WaitForCompletionAsync();
_logger.LogInformation("License cleared from Azure Key Vault");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clear license from Azure Key Vault");
return false;
}
}
}
Example: Redis Cache Provider
public class RedisLicenseKeyProvider : ILicenseKeyProvider
{
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<RedisLicenseKeyProvider> _logger;
private const string RedisKey = "app:license:key";
public RedisLicenseKeyProvider(
IConnectionMultiplexer redis,
ILogger<RedisLicenseKeyProvider> logger)
{
_redis = redis;
_logger = logger;
}
public async Task<string?> GetLicenseKeyAsync()
{
try
{
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(RedisKey);
if (value.HasValue)
{
_logger.LogDebug("Retrieved license from Redis cache");
return value.ToString();
}
_logger.LogWarning("License not found in Redis cache");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve license from Redis");
return null;
}
}
public async Task<bool> StoreLicenseKeyAsync(string licenseKey)
{
try
{
var db = _redis.GetDatabase();
await db.StringSetAsync(RedisKey, licenseKey);
_logger.LogInformation("License stored successfully in Redis cache");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to store license in Redis");
return false;
}
}
public async Task<bool> ClearLicenseKeyAsync()
{
try
{
var db = _redis.GetDatabase();
await db.KeyDeleteAsync(RedisKey);
_logger.LogInformation("License cleared from Redis cache");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clear license from Redis");
return false;
}
}
}
Registering Custom Providers
Option 1: Using UseCustomProvider<T>() (Type Registration)
Registers a provider type that will be resolved through DI. This is the recommended approach when your provider needs dependencies injected.
// Provider with DI dependencies
builder.Services.AddKnmLicenseValidator(validator =>
{
validator
.WithValidationMode(ValidationMode.Hybrid)
.WithApiSettings("https://api.company.com", "api-key")
.UseCustomProvider<DatabaseLicenseKeyProvider>(); // โ DI resolves dependencies
});
// Azure Key Vault provider with DI
builder.Services.AddAzureClients(clientBuilder =>
{
clientBuilder.AddSecretClient(new Uri("https://your-vault.vault.azure.net/"));
});
builder.Services.AddKnmLicenseValidator(validator =>
{
validator.UseCustomProvider<AzureKeyVaultLicenseProvider>();
});
Key characteristics:
- โ
Service Lifetime:
Scoped- New instance per HTTP request - โ Dependency Injection: Full DI support for constructor parameters
- โ Lazy Initialization: Instance created only when needed
Option 2: Using UseProvider(instance) (Instance Registration)
Registers a pre-configured instance. Use this when you need full control over the instance creation or when the provider doesn't need DI.
// Pre-configured instance
var keyVaultUrl = builder.Configuration["KeyVault:Url"];
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var provider = new AzureKeyVaultLicenseProvider(
new SecretClient(new Uri(keyVaultUrl!), new DefaultAzureCredential()),
loggerFactory.CreateLogger<AzureKeyVaultLicenseProvider>()
);
builder.Services.AddKnmLicenseValidator(validator =>
{
validator
.WithValidationMode(ValidationMode.OnlineOnly)
.WithApiSettings("https://api.company.com", "api-key")
.UseProvider(provider); // โ Pre-configured instance
});
// Simple file-based provider
var fileProvider = new FileLicenseKeyProvider("./license-keys/current.key");
builder.Services.AddKnmLicenseValidator(validator =>
{
validator.UseProvider(fileProvider);
});
Key characteristics:
- โ
Service Lifetime:
Singleton- Same instance across application - โ Manual Control: Full control over instance creation and configuration
- โ Eager Initialization: Instance created immediately
Comparison: Type vs Instance Registration
| Feature | UseCustomProvider<T>() |
UseProvider(instance) |
|---|---|---|
| Lifetime | Scoped | Singleton |
| DI Support | โ Full support | โ Manual construction |
| Initialization | Lazy | Eager |
| Use Case | Database, HTTP clients | Simple providers, pre-configured services |
| Best For | ASP.NET Core apps | Console apps, Workers |
Working with Extra Data
public async Task ProcessLicenseDataAsync()
{
var result = await _validator.ValidateLicenseAsync();
if (!result.IsValid) return;
// Check for specific features
var hasReporting = result.HasFeature("reporting", "modules");
var hasExport = result.HasFeature("export", "modules");
// Extract typed data from ExtraData JSON
var maxUsers = result.GetExtraDataValue<int>("maxUsers");
var companyName = result.GetExtraDataValue<string>("companyName");
_logger.LogInformation(
"License for {Company} allows {MaxUsers} users",
companyName,
maxUsers
);
}
Force Specific Validation Mode
// Force online validation (requires internet)
var onlineResult = await _validator.ValidateOnlineAsync("LICENSE-KEY-HERE");
// Force offline validation (uses local files)
var offlineResult = await _validator.ValidateOfflineAsync();
// Get cached result without validation
var cached = _validator.GetCachedResult();
if (cached?.IsValid == true)
{
_logger.LogInformation("Using cached validation result");
}
// Clear cache to force fresh validation
_validator.ClearCache();
Custom HTTP Client Configuration
builder.Services.AddKnmLicenseValidator(validator =>
{
validator
.WithValidationMode(ValidationMode.OnlineOnly)
.WithApiSettings("https://api.company.com", "api-key")
.WithHttpClient(client =>
{
client.DefaultRequestHeaders.Add("X-Custom-Header", "value");
client.DefaultRequestHeaders.Add("X-Application-Id", "my-app");
client.Timeout = TimeSpan.FromMinutes(2);
});
});
Context-Aware Configuration
builder.Services.AddKnmLicenseValidator(validator =>
{
validator
.WithValidationMode(ValidationMode.Hybrid)
.WithApiSettings("https://api.company.com", "api-key")
.WithContext(
softwareId: 42,
userId: "user@example.com",
installationId: Guid.NewGuid());
});
ASP.NET Core Middleware Example
public class LicenseValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LicenseValidationMiddleware> _logger;
public LicenseValidationMiddleware(
RequestDelegate next,
ILogger<LicenseValidationMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(
HttpContext context,
ILicenseValidator validator)
{
var result = await validator.ValidateLicenseAsync();
if (!result.IsValid)
{
_logger.LogWarning("Request blocked: Invalid license");
context.Response.StatusCode = 402; // Payment Required
await context.Response.WriteAsJsonAsync(new
{
error = "License validation failed",
message = result.ErrorMessage
});
return;
}
await _next(context);
}
}
Register in Program.cs:
app.UseMiddleware<LicenseValidationMiddleware>();
Blazor Component Example
@inject ILicenseValidator LicenseValidator
<div class="license-status">
@if (_isValidating)
{
<p>Validating license...</p>
}
else if (_licenseResult?.IsValid == true)
{
<div class="alert alert-success">
<strong>License Active</strong>
<p>Valid until: @_licenseResult.LicenseInfo?.ExpirationDate.ToShortDateString()</p>
</div>
}
else
{
<div class="alert alert-danger">
<strong>License Invalid</strong>
<p>@_licenseResult?.ErrorMessage</p>
</div>
}
</div>
@code {
private LicenseValidationResult? _licenseResult;
private bool _isValidating = true;
protected override async Task OnInitializedAsync()
{
_licenseResult = await LicenseValidator.ValidateLicenseAsync();
_isValidating = false;
}
}
โ๏ธ Configuration Options
Fluent Builder Methods
| Method | Parameters | Description |
|---|---|---|
WithValidationMode() |
ValidationMode |
Sets validation mode: OfflineOnly, OnlineOnly, or Hybrid |
WithApiSettings() |
baseUrl, apiKey, timeout? |
Configures online API validation settings |
WithOfflineFiles() |
directory, publicKey?, license? |
Sets paths for offline license files |
WithCacheExpiration() |
TimeSpan |
Sets cache expiration duration |
WithFallbackBehavior() |
toOffline, toOnline |
Configures fallback behavior between modes |
WithSecuritySettings() |
strict, logAttempts |
Configures security and logging settings |
WithContext() |
softwareId, userId?, installationId? |
Sets application context information |
WithStartupValidation() |
- | Enables validation on application startup |
WithHttpClient() |
Action<HttpClient> |
Customizes HTTP client configuration |
UseCustomProvider<T>() |
- | Registers custom license key provider type (Scoped lifetime, DI-enabled) |
UseProvider() |
ILicenseKeyProvider |
Registers specific provider instance (Singleton lifetime) |
Configuration Properties
| Property | Type | Default | Description |
|---|---|---|---|
Mode |
ValidationMode |
Hybrid |
Validation mode |
ApiBaseUrl |
string? |
null |
Base URL for online validation API |
ApiKey |
string? |
null |
API key for authentication |
ApiTimeout |
TimeSpan |
00:00:30 |
HTTP request timeout |
LicenseDirectory |
string |
./Licenses |
Directory containing license files |
PublicKeyFileName |
string |
PublicKey.xml |
RSA public key filename |
LicenseFileName |
string |
License.txt |
License file name |
PublicKeyPath |
string? |
null |
Absolute path to public key |
LicensePath |
string? |
null |
Absolute path to license file |
CacheExpiration |
TimeSpan |
00:30:00 |
Cache expiration duration |
ValidateOnStartup |
bool |
true |
Validate on application startup |
FallbackToOffline |
bool |
true |
Fallback to offline if online fails |
FallbackToOnline |
bool |
false |
Fallback to online if offline fails |
StrictValidation |
bool |
true |
Enforce strict validation checks |
LogValidationAttempts |
bool |
true |
Log validation attempts |
SoftwareId |
int |
0 |
Software identifier |
UserId |
string? |
null |
User identifier |
InstallationId |
Guid? |
null |
Installation identifier |
๐ Security Architecture
The library implements a three-layer security model:
- RSA Signature Verification: Ensures license authenticity using public key cryptography (2048-bit RSA)
- AES-256 Encryption: Protects license data confidentiality with industry-standard encryption
- HMAC-SHA256 Integrity Check: Validates data hasn't been tampered with (online mode only)
License File Format
-----START LICENSE DATA-----
[Base32-encoded encrypted payload]
-----STOP LICENSE DATA-----
The payload contains:
- Expiration date (UTC ticks format)
- Software ID (integer identifier)
- Client ID (integer identifier)
- Cryptographic salt (unique per license)
- Extra data (JSON format for custom fields)
Security Best Practices
- Keep private keys secure: Never distribute RSA private keys with your application
- Use HTTPS: Always use HTTPS for online validation API endpoints
- Rotate API keys: Periodically rotate API keys for enhanced security
- Monitor failed attempts: Log and monitor failed validation attempts
- Secure storage: Use secure storage mechanisms (Azure Key Vault, etc.) for license keys
๐งช Testing
Unit Test Example
[Fact]
public async Task ValidateLicense_WithValidLicense_ReturnsSuccess()
{
// Arrange
var services = new ServiceCollection();
services.AddKnmLicenseValidator(validator =>
{
validator
.WithValidationMode(ValidationMode.OfflineOnly)
.WithOfflineFiles("./TestLicenses");
});
var provider = services.BuildServiceProvider();
var validator = provider.GetRequiredService<ILicenseValidator>();
// Act
var result = await validator.ValidateOfflineAsync();
// Assert
Assert.True(result.IsValid);
Assert.NotNull(result.LicenseInfo);
Assert.Equal(ValidationSource.Offline, result.Source);
}
Integration Test Example
public class LicenseValidationIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public LicenseValidationIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Endpoint_WithValidLicense_Returns200()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/license/status");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("valid", content, StringComparison.OrdinalIgnoreCase);
}
}
๐ Troubleshooting
License file not found
Problem: Application can't find license files.
Solution:
- Ensure
LicenseDirectorypoints to the correct location - Verify files exist:
PublicKey.xmlandLicense.txt - Check file permissions (read access required)
- Use absolute paths with
PublicKeyPathandLicensePathproperties
Online validation fails
Problem: Online validation returns errors or times out.
Solution:
- Verify
ApiBaseUrlis correct and reachable - Check
ApiKeyis valid and not expired - Ensure firewall allows outbound HTTPS connections
- Increase
ApiTimeout:.WithApiSettings(baseUrl, apiKey, TimeSpan.FromSeconds(60))
RSA signature verification failed
Problem: License validation fails with RSA signature error.
Solution:
- Ensure public key file matches the private key used to sign the license
- Verify license file hasn't been modified or corrupted
- Check file encoding (UTF-8 expected)
Cache not working
Problem: Validation doesn't use cached results.
Solution:
- Adjust cache expiration:
.WithCacheExpiration(TimeSpan.FromHours(24)) - Verify cache isn't being cleared unexpectedly
- Use
GetCachedResult()to inspect cache state
๐ Performance Considerations
- Caching: Validation results are cached to minimize API calls and file I/O
- Singleton Pattern: Core validators use appropriate lifetimes for optimal performance
- Async Operations: All validation methods are fully asynchronous
- Memory Efficient: Minimal memory footprint with no database dependencies
- Fast Startup: Optional startup validation runs in background, non-blocking
Typical Performance:
- Offline validation: < 10ms (with warm cache)
- Online validation: 50-500ms (network dependent)
- Cached result retrieval: < 1ms
๐ Related Packages
- KNM.CryptoHelper - Cryptographic utilities for encryption and signing
- KNM.RazorComponents - Reusable Blazor component library
Made with โค๏ธ by KoNiMa
Built for enterprise applications requiring robust license management
No packages depend on KNM.LicenseValidator.
.NET 10.0
- KNM.CryptoHelper (>= 1.3.9)
- Microsoft.EntityFrameworkCore (>= 10.0.1)
- Microsoft.EntityFrameworkCore.SqlServer (>= 10.0.1)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Http (>= 10.0.1)
- Microsoft.Extensions.Localization.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.1)
- System.Security.Cryptography.ProtectedData (>= 10.0.1)