AuthFlow.LicenseClient 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package AuthFlow.LicenseClient --version 0.1.0
                    
NuGet\Install-Package AuthFlow.LicenseClient -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AuthFlow.LicenseClient" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AuthFlow.LicenseClient" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="AuthFlow.LicenseClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AuthFlow.LicenseClient --version 0.1.0
                    
#r "nuget: AuthFlow.LicenseClient, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AuthFlow.LicenseClient@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AuthFlow.LicenseClient&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=AuthFlow.LicenseClient&version=0.1.0
                    
Install as a Cake Tool

AuthFlow.LicenseClient

.NET SDK for integrating ASP.NET Core / Blazor applications (RadissonConnect, PI, Osiris runtime, InterfaceX3, FNEBridge, FneManuelInvoiceApp, ...) with the AuthFlow license server.

Provides hybrid online + offline license validation:

  • Verifies the Ed25519 signature of a license token locally, without any network call (via NSec.Cryptography, libsodium bindings).
  • Calls the AuthFlow server (POST /api/validate) to get the current server-side status (catches revocations/expirations that happened after the token was issued).
  • Falls back to the last successful validation, cached locally, for a configurable grace period (default 7 days) when the server is unreachable - but always blocks immediately if the signature is invalid or the token's embedded status is REVOKED/SUSPENDED, or its expiresAt has passed.
  • Sends periodic heartbeats (POST /api/heartbeat) via a hosted BackgroundService.

Target framework: net8.0 (a good default given the target apps span .NET 6 through .NET 10; a class library on net8.0 can still be referenced by newer TFMs, and net8.0 is the actively-supported LTS most of these apps are expected to converge on).

Server contract this SDK implements

(Already built server-side - see the repo root README and src/lib/license-token.ts.)

  • Token format: base64url(payload JSON) + "." + base64url(Ed25519 signature), where the signature covers the exact bytes of the base64url-encoded payload segment (JWS-style), not the re-serialized JSON.
  • Payload fields: licenseId, productId, customerId, type, status, features, maxActivations, expiresAt, usageQuota, keyId, issuedAt.
  • keyId allows key rotation: the server (and this SDK) can hold several active public keys simultaneously.
  • Endpoints consumed: POST /api/activate, POST /api/validate, POST /api/heartbeat, GET /api/updates/check.

Installation

This package isn't published to nuget.org yet. For now, reference it locally from the monorepo:

Project reference (recommended while iterating in the same repo/monorepo):

<ItemGroup>
  <ProjectReference Include="..\path\to\AuthFlow\sdk\dotnet\AuthFlow.LicenseClient\AuthFlow.LicenseClient.csproj" />
</ItemGroup>

Local NuGet package (if you want to consume it like a real package, e.g. from a separate repo):

dotnet build sdk/dotnet/AuthFlow.LicenseClient -c Release
# Produces sdk/dotnet/AuthFlow.LicenseClient/bin/Release/AuthFlow.LicenseClient.0.1.0.nupkg
dotnet nuget add source "C:\path\to\local-nuget-feed" -n authflow-local
dotnet nuget push sdk/dotnet/AuthFlow.LicenseClient/bin/Release/AuthFlow.LicenseClient.0.1.0.nupkg -s authflow-local
dotnet add package AuthFlow.LicenseClient -s authflow-local

Publishing to nuget.org is a later step, once the SDK's public API has stabilized across a few real integrations.

Getting your product's public key

Public keys are generated server-side (scripts/generate-keys.ts, Ed25519). The private key never leaves the server (LICENSE_SIGNING_PRIVATE_KEY env var on Vercel); only the base64-encoded SPKI DER public key (LICENSE_SIGNING_PUBLIC_KEYS, keyed by keyId) is safe to embed in this SDK's configuration.

Usage (ASP.NET Core)

using AuthFlow.LicenseClient.DependencyInjection;

services.AddAuthFlowLicense(options =>
{
    options.ProductId = "<AuthFlow Product.id>";
    options.PublicKeys = new Dictionary<string, string>
    {
        ["key1"] = "<base64 SPKI DER public key>",
    };
    options.ServerUrl = "https://authflow.vercel.app";
    options.GracePeriod = TimeSpan.FromDays(7);      // default: 7 days
    options.HeartbeatInterval = TimeSpan.FromHours(1); // default: 1 hour
    options.UpdateChannel = "STABLE"; // or "BETA" for beta builds
});

Then, wherever you need to gate access (a middleware, a startup check, a Blazor component, ...):

public class LicenseGate
{
    private readonly ILicenseValidator _licenseValidator;

    public LicenseGate(ILicenseValidator licenseValidator) => _licenseValidator = licenseValidator;

    public async Task<bool> EnsureLicensedAsync(string licenseKey)
    {
        var result = await _licenseValidator.CheckAsync(licenseKey);
        if (!result.IsValid)
        {
            // Block the app: result.Reason explains why (invalid signature,
            // revoked, expired, grace period elapsed, ...).
            return false;
        }

        if (result.IsOffline)
        {
            // Optional: surface a "running in offline mode" banner/log,
            // since result.IsValid was determined without reaching the server.
        }

        return true;
    }
}

AddAuthFlowLicense also registers a LicenseHeartbeatService (IHostedService) that automatically sends a periodic heartbeat once a license has been validated online at least once (it needs a real activationId, which is only known after a successful /api/activate or /api/validate call).

It also registers IUpdateChecker, which consuming apps can call explicitly when they want to surface update information without coupling it to every license validation:

public class UpdateBannerService
{
    private readonly IUpdateChecker _updateChecker;

    public UpdateBannerService(IUpdateChecker updateChecker) => _updateChecker = updateChecker;

    public Task<UpdateCheckResult> CheckAsync() =>
        _updateChecker.CheckForUpdateAsync("3.5.0");
}

IUpdateChecker is intentionally separate from ILicenseValidator / POST /api/validate:

  • /api/validate is security-sensitive and bound to a specific license + device;
  • /api/updates/check is product-wide release metadata, not entitlement state;
  • apps typically check for updates less often than they validate licenses;
  • keeping the calls separate avoids changing the already-shipped validation hot path and keeps transient update-check failures non-fatal.

To activate a new device for the first time (e.g. on first run / after entering a license key), call the API client directly:

public class FirstRunActivation
{
    private readonly IAuthFlowApiClient _apiClient;
    private readonly IDeviceFingerprintProvider _fingerprintProvider;

    public FirstRunActivation(IAuthFlowApiClient apiClient, IDeviceFingerprintProvider fingerprintProvider)
    {
        _apiClient = apiClient;
        _fingerprintProvider = fingerprintProvider;
    }

    public Task<ActivateResponse> ActivateAsync(string licenseKey) =>
        _apiClient.ActivateAsync(new ActivateRequest
        {
            LicenseKey = licenseKey,
            DeviceFingerprint = _fingerprintProvider.GetFingerprint(),
            DeviceName = Environment.MachineName,
        });
}

Known limitations / trade-offs

These are deliberate, documented compromises rather than oversights - revisit them if a given integration needs stronger guarantees.

Security considerations / threat model

AuthFlow.LicenseClient improves licensing integrity, but it does not create an unbreakable client-side security boundary.

The SDK can be decompiled and patched

This SDK ships as a normal .NET assembly inside the customer application. A sufficiently motivated attacker with local access to the machine/binaries can decompile it, modify it, and patch the calling application.

Examples of realistic attacks:

  • patching LicenseValidator.CheckAsync so it always returns IsValid = true;
  • bypassing the code path that blocks app startup on an invalid result;
  • replacing the assembly with a modified build in an environment the attacker fully controls.

This is a fundamental limitation of all client-side licensing enforcement, not a bug specific to this SDK.

The real security boundary is the server

The authoritative source of truth is the AuthFlow server, especially POST /api/validate.

  • Online validation can reflect revocations, suspensions and expirations that happened after token issuance.
  • Offline validation/cache exists for availability and resilience during legitimate connectivity gaps.
  • The grace period is therefore a usability / availability trade-off, not a security feature.

If the client machine is fully under attacker control, any purely local decision can eventually be bypassed. The server is where policy enforcement is most trustworthy.

What the Ed25519 signature does and does not protect

The Ed25519 signature verification is still valuable: it prevents a tampered or forged license token payload from being accepted by an unmodified SDK.

However, it does not prevent an attacker from patching the SDK itself and skipping that verification entirely. In other words:

  • it protects against tampered license data;
  • it does not protect against a tampered client binary.

That distinction is intentional and should be communicated honestly to any team integrating this SDK.

Why no extra anti-tampering mechanism was added here

This phase deliberately avoids heavy anti-tamper engineering. There is no existing strong-name / Authenticode signing pipeline in this project today, and adding a custom integrity layer inside the SDK would provide limited value against attackers who already control the binary and can patch out the check as well.

For now, the pragmatic approach is:

  • document the limitation clearly;
  • keep server-side validation authoritative;
  • treat offline mode as best-effort continuity, not a hard security guarantee.

Device fingerprinting is not truly hardware-bound

There is no single hardware identifier reliably available from managed .NET code, license-free, across Windows/Linux/macOS. DeviceFingerprintProvider combines Environment.MachineName with a random GUID generated once and persisted to a local file (%LOCALAPPDATA%/AuthFlow/device-id or the platform equivalent - see below). Consequences:

  • Stable across app restarts on the same machine/user profile.
  • Not stable across a reinstalled OS, a cleared app-data directory, or a new VM/container provisioned from the same golden image - each of those will look like a "new device" (or, for cloned images sharing an initial device-id file, briefly the same device, until each regenerates its own id after any event that clears local state).
  • If stronger anti-cloning binding is needed later, consider a platform-specific approach (e.g. TPM-backed identifiers, WMI on Windows, /etc/machine-id on Linux) behind the same IDeviceFingerprintProvider interface - swappable without touching the rest of the SDK.

Local cache encryption is Windows-only (DPAPI)

LicenseCacheStore encrypts the on-disk cache file with DPAPI (System.Security.Cryptography.ProtectedData, user scope) on Windows only - DPAPI is a Windows API and throws PlatformNotSupportedException on Linux/macOS. On non-Windows platforms, the SDK detects this via RuntimeInformation.IsOSPlatform(OSPlatform.Windows) and falls back to storing the same JSON cache unencrypted, relying on OS file permissions (the user profile directory) alone.

This is an acceptable trade-off here because the cached payload is just a previously-validated license token plus a timestamp - not a credential or secret - and the signature check plus periodic online re-validation still bound the practical impact of a user tampering with their own local cache file. If a cross-platform encrypted-at-rest cache becomes a hard requirement, swap in a library like Sodium.Core with a locally-managed symmetric key, or System.Security.Cryptography.Aes with a key derived from a per-machine secret.

%LOCALAPPDATA% equivalent on non-Windows

Both the device id and the license cache live under Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), which .NET maps to ~/.local/share on Linux/macOS - a writable, per-user directory, but not necessarily the platform-idiomatic location on every OS.

Building and testing

cd sdk/dotnet
dotnet build AuthFlow.LicenseClient.slnx
dotnet test AuthFlow.LicenseClient.slnx
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on AuthFlow.LicenseClient:

Package Downloads
AuthFlow.AppKit

Reusable AuthFlow license activation/validation + in-app update-check integration for ASP.NET Core Blazor Server apps (Kestrel + Windows Service). Extracted from the FneManuelInvoiceApp pilot integration - see https://github.com/Brackford-0brien/AuthFlow-AppKit for docs.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 133 7/6/2026
0.1.0 112 7/6/2026