KNM.LicenseValidator 1.1.4

KNM License Validator

.NET

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 ILicenseKeyProvider for 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.4" />

๐Ÿš€ Quick Start

1. Minimal Configuration

using KNM.LicenseValidator.Extensions;
using KNM.LicenseValidator.Configuration;

var builder = WebApplication.CreateBuilder(args);

// Simple offline validation
builder.Services
    .AddKnmLicenseValidator()
    .WithValidationMode(ValidationMode.OfflineOnly)
    .WithOfflineFiles("./Licenses")
    .Build();

var app = builder.Build();
app.Run();
builder.Services
    .AddKnmLicenseValidator()
    .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()
    .Build();

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:

builder.Services
    .AddKnmLicenseValidator(builder.Configuration)
    .Build();

// Or with custom provider
builder.Services
    .AddKnmLicenseValidator(builder.Configuration)
    .UseCustomProvider<DatabaseLicenseKeyProvider>()
    .WithStartupValidation()
    .Build();

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()
    .WithValidationMode(ValidationMode.Hybrid)
    .WithApiSettings("https://api.company.com", "api-key")
    .UseCustomProvider<DatabaseLicenseKeyProvider>() // โ† DI resolves dependencies
    .Build();

// Azure Key Vault provider with DI
builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddSecretClient(new Uri("https://your-vault.vault.azure.net/"));
});

builder.Services
    .AddKnmLicenseValidator()
    .UseCustomProvider<AzureKeyVaultLicenseProvider>()
    .Build();

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 provider = new AzureKeyVaultLicenseProvider(
    new SecretClient(new Uri(keyVaultUrl!), new DefaultAzureCredential()),
    loggerFactory.CreateLogger<AzureKeyVaultLicenseProvider>()
);

builder.Services
    .AddKnmLicenseValidator()
    .WithValidationMode(ValidationMode.OnlineOnly)
    .WithApiSettings("https://api.company.com", "api-key")
    .UseProvider(provider) // โ† Pre-configured instance
    .Build();

// Simple file-based provider
var fileProvider = new FileLicenseKeyProvider("./license-keys/current.key");
builder.Services
    .AddKnmLicenseValidator()
    .UseProvider(fileProvider)
    .Build();

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()
    .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);
    })
    .Build();

Context-Aware Configuration

builder.Services
    .AddKnmLicenseValidator()
    .WithValidationMode(ValidationMode.Hybrid)
    .WithApiSettings("https://api.company.com", "api-key")
    .WithContext(
        softwareId: 42,
        userId: "user@example.com",
        installationId: Guid.NewGuid())
    .Build();

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:

  1. RSA Signature Verification: Ensures license authenticity using public key cryptography (2048-bit RSA)
  2. AES-256 Encryption: Protects license data confidentiality with industry-standard encryption
  3. 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()
        .WithValidationMode(ValidationMode.OfflineOnly)
        .WithOfflineFiles("./TestLicenses")
        .Build();
    
    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 LicenseDirectory points to the correct location
  • Verify files exist: PublicKey.xml and License.txt
  • Check file permissions (read access required)
  • Use absolute paths with PublicKeyPath and LicensePath properties

Online validation fails

Problem: Online validation returns errors or times out.

Solution:

  1. Verify ApiBaseUrl is correct and reachable
  2. Check ApiKey is valid and not expired
  3. Ensure firewall allows outbound HTTPS connections
  4. 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
  • 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.

Version Downloads Last updated
1.1.5 0 09/01/2026
1.1.4 0 09/01/2026
1.1.3 0 09/01/2026
1.1.2 0 09/01/2026
1.1.1 0 09/01/2026
1.0.9 2 03/11/2025
1.0.8 0 03/11/2025
1.0.7 1 03/11/2025
1.0.6 1 03/11/2025
1.0.4 1 03/11/2025