KNM.LicenseValidator 1.1.1

Ecco il README.md aggiornato con il nuovo pattern fluent builder e le configurazioni corrette:

# KNM License Validator

[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)

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

```bash
# Add your private feed source (one-time setup)
dotnet nuget add source "https://your-private-feed-url" \
    --name "KNM-Internal" \
    --username "your-username" \
    --password "your-token" \
    --store-password-in-clear-text

# 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.0.0" />

πŸš€ 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

Implement ILicenseKeyProvider for custom storage (database, Azure Key Vault, etc.):

public class DatabaseLicenseKeyProvider : ILicenseKeyProvider
{
    private readonly ApplicationDbContext _context;

    public DatabaseLicenseKeyProvider(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<string?> GetLicenseKeyAsync()
    {
        var license = await _context.Licenses
            .FirstOrDefaultAsync(l => l.IsActive);
        return license?.Key;
    }

    public async Task<bool> StoreLicenseKeyAsync(string licenseKey)
    {
        var license = new License { Key = licenseKey, IsActive = true };
        _context.Licenses.Add(license);
        return await _context.SaveChangesAsync() > 0;
    }

    public async Task<bool> ClearLicenseKeyAsync()
    {
        var licenses = await _context.Licenses.ToListAsync();
        _context.Licenses.RemoveRange(licenses);
        return await _context.SaveChangesAsync() > 0;
    }
}

Register with fluent API:

builder.Services
    .AddKnmLicenseValidator()
    .WithValidationMode(ValidationMode.Hybrid)
    .WithApiSettings("https://api.company.com", "api-key")
    .UseCustomProvider<DatabaseLicenseKeyProvider>()
    .Build();

// Or use a specific instance
var provider = new AzureKeyVaultProvider(keyVaultUrl);
builder.Services
    .AddKnmLicenseValidator()
    .WithValidationMode(ValidationMode.OnlineOnly)
    .WithApiSettings("https://api.company.com", "api-key")
    .UseProvider(provider)
    .Build();

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");
    })
    .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
UseProvider() ILicenseKeyProvider Registers specific provider instance

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

🀝 Contributing

We welcome contributions! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

  • KNM.CryptoHelper - Cryptographic utilities for encryption and signing
  • KNM.RazorComponents - Reusable Blazor component library

πŸ“§ Support

For issues, questions, or suggestions:

πŸ—ΊοΈ Roadmap

  • Support for .NET 11 when released
  • Hardware ID binding support
  • Multi-tenant license management
  • Cloud-based license server reference implementation
  • License renewal automation
  • Enhanced audit logging

πŸ“ Changelog

Version 1.0.0 (Initial Release)

  • Fluent configuration API with builder pattern
  • Hybrid offline/online validation
  • Triple-layer security (RSA + AES + HMAC)
  • Custom license key providers
  • Validation result caching
  • Startup validation service
  • Full .NET 10 and AOT support

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