AuthFlow.LicenseClient
0.1.0
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
<PackageReference Include="AuthFlow.LicenseClient" Version="0.1.0" />
<PackageVersion Include="AuthFlow.LicenseClient" Version="0.1.0" />
<PackageReference Include="AuthFlow.LicenseClient" />
paket add AuthFlow.LicenseClient --version 0.1.0
#r "nuget: AuthFlow.LicenseClient, 0.1.0"
#:package AuthFlow.LicenseClient@0.1.0
#addin nuget:?package=AuthFlow.LicenseClient&version=0.1.0
#tool nuget:?package=AuthFlow.LicenseClient&version=0.1.0
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 itsexpiresAthas passed. - Sends periodic heartbeats (
POST /api/heartbeat) via a hostedBackgroundService.
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. keyIdallows 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/validateis security-sensitive and bound to a specific license + device;/api/updates/checkis 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.CheckAsyncso it always returnsIsValid = 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-idon Linux) behind the sameIDeviceFingerprintProviderinterface - 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 | Versions 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. |
-
net8.0
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- NSec.Cryptography (>= 24.4.0)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
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.