PWFAuth 1.0.1

dotnet add package PWFAuth --version 1.0.1
                    
NuGet\Install-Package PWFAuth -Version 1.0.1
                    
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="PWFAuth" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PWFAuth" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="PWFAuth" />
                    
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 PWFAuth --version 1.0.1
                    
#r "nuget: PWFAuth, 1.0.1"
                    
#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 PWFAuth@1.0.1
                    
#: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=PWFAuth&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=PWFAuth&version=1.0.1
                    
Install as a Cake Tool

PWF Auth for .NET

Official client for PWF Auth — license keys, user accounts, hardware-ID binding, encrypted sessions with a server-side kill switch, free trials, remote content, and OTA update checks.

dotnet add package PWFAuth

Targets netstandard2.0 (works on .NET Framework 4.6.2+, so WinForms/WPF and VB.NET desktop apps are first-class) and net8.0.

Quick start

using PWFAuth;

var client = new PwfClient("your-64-char-app-secret");

// The moment an admin bans, pauses, expires, resets or revokes the key — or the
// server becomes unreachable — this fires. Sign the user out here.
client.SessionEnded += (sender, e) =>
{
    Console.WriteLine($"Session ended: {e.ErrorCode} — {e.Message}");
    Environment.Exit(0);
};

var login = await client.LoginAsync("XXXXX-XXXXX-XXXXX-XXXXX");
if (!login.Success)
{
    Console.WriteLine(login.Message);   // safe to show the user
    return;
}

client.StartHeartbeat();   // keeps the session alive AND enforces the kill switch

VB.NET:

Dim client As New PwfClient("your-64-char-app-secret")
AddHandler client.SessionEnded, Sub(s, e)
                                    MessageBox.Show(e.Message)
                                    Application.Exit()
                                End Sub

Dim login = Await client.LoginAsync("XXXXX-XXXXX-XXXXX-XXXXX")
If login.Success Then client.StartHeartbeat()

Why the heartbeat matters

StartHeartbeat() is not optional bookkeeping — it is the enforcement point.

  • The server drops a session that stops beating, so a client that never beats loses nothing but also never learns it was revoked.
  • Every beat re-checks the license: ban, pause, expiry, HWID reset, deletion and maintenance mode all end the session on the very next beat.
  • If the server is unreachable for MaxHeartbeatFailures beats in a row (3 by default), the client ends the session itself with NETWORK_LOST. Without that, blocking the license domain in a firewall would keep the application running forever.

What the login reply contains

A successful LoginAsync carries the whole licence state, so you rarely need a second call to show the user what they have.

Field Type Notes
user.license_key string The key that was activated
user.key_type string days, hours, lifetime, …
user.duration number Length in units of key_type
user.hwid string The machine this session is bound to
user.activated_at string UTC ISO 8601, first activation
user.expires_at string | null UTC ISO 8601. null means lifetime
user.days_remaining number | null null for lifetime keys
user.status string active
features object Per-key feature flags you set in the dashboard
app.name / app.version / app.message string Your app's name, current version, login message
texts / slides object / array Remote strings and announcement slides
session_id string Also held internally by the client
heartbeat_interval number Seconds; StartHeartbeat() already honours it

expires_at and days_remaining are null on lifetime keys — always check before formatting, or a lifetime customer sees a crash instead of "never expires".

using System.Text.Json;

if (login.TryGetProperty("user", out JsonElement user))
{
    string? key    = user.GetProperty("license_key").GetString();
    string? status = user.GetProperty("status").GetString();

    // The ValueKind check is required, not defensive padding: on .NET Framework
    // TryGetDateTime *throws* on a JSON null instead of returning false, and
    // expires_at is null for every lifetime key.
    if (user.TryGetProperty("expires_at", out JsonElement exp)
        && exp.ValueKind == JsonValueKind.String
        && exp.TryGetDateTime(out DateTime expiresUtc))
    {
        int left = user.TryGetProperty("days_remaining", out JsonElement days)
                   && days.ValueKind == JsonValueKind.Number ? days.GetInt32() : 0;

        Console.WriteLine($"{key} — expires {expiresUtc:yyyy-MM-dd HH:mm} UTC, {left} day(s) left");
        Console.WriteLine($"Local time: {expiresUtc.ToLocalTime():g}");
    }
    else
    {
        Console.WriteLine($"{key} — lifetime licence ({status})");
    }
}

// Feature flags — whatever you defined for this key in the dashboard
foreach (var f in login.GetStringMap("features"))
    Console.WriteLine($"{f.Key} = {f.Value}");

VB.NET — the same thing for a WinForms label:

Imports System.Text.Json

Dim user As JsonElement
If login.TryGetProperty("user", user) Then
    Dim expEl As JsonElement, daysEl As JsonElement
    Dim expiresUtc As DateTime

    ' The ValueKind check is required: on .NET Framework TryGetDateTime throws
    ' on a JSON null rather than returning False, and lifetime keys send null.
    If user.TryGetProperty("expires_at", expEl) AndAlso
       expEl.ValueKind = JsonValueKind.String AndAlso
       expEl.TryGetDateTime(expiresUtc) Then
        Dim left As Integer = 0
        If user.TryGetProperty("days_remaining", daysEl) AndAlso
           daysEl.ValueKind = JsonValueKind.Number Then left = daysEl.GetInt32()

        lblLicense.Text = String.Format("Expires {0:yyyy-MM-dd} — {1} day(s) left",
                                        expiresUtc.ToLocalTime(), left)

        If left <= 7 Then lblLicense.ForeColor = Color.OrangeRed   ' nudge them to renew
    Else
        lblLicense.Text = "Lifetime licence"          ' no expiry to show
    End If
End If

' Gate a feature on a per-key flag
Dim features = login.GetStringMap("features")
btnProExport.Enabled = features.ContainsKey("pro_tier") AndAlso features("pro_tier") = "True"

Nothing here is cached by the client — re-read it from the login response you already hold, or call CheckKeyAsync(key) later for a fresh read without consuming a seat.

What else it does

var status  = await client.CheckKeyAsync(key);        // no session, no seat consumed
var info    = await client.GetAppInfoAsync();         // name, version, download URL, socials
var texts   = await client.GetTextsAsync();           // remote strings, per-key overrides
var slides  = await client.GetSlidesAsync();          // announcement slides
var update  = await client.CheckUpdateAsync("1.4.2"); // OTA: version, sha256, download URL
var trial   = await client.CreateTrialAsync();        // free trial for this machine
await client.RequestHardwareResetAsync(key, "New laptop");

// User accounts (the username/password half of the platform)
await client.RegisterAccountAsync("alice", "s3cret", "alice@example.com");
await client.AccountLoginAsync("alice", "s3cret");

Endpoints this client does not wrap yet are still reachable — PostEnvelopeAsync, GetEnvelopeAsync and PostPlainAsync are public, and CryptoEnvelope is too.

Reading results

PwfResponse exposes Success, ErrorCode and Message, plus typed getters and the raw JsonElement for endpoint-specific fields:

if (!login.Success && login.ErrorCode == PwfErrorCodes.HwidMismatch)
    ShowHardwareResetDialog();

var welcome = (await client.GetTextsAsync()).GetStringMap("texts")["welcome_message"];

var check = await client.CheckUpdateAsync("1.4.2");
if (check.GetBoolean("update_available", false) &&
    check.TryGetProperty("update", out var upd))
{
    Console.WriteLine(upd.GetProperty("version").GetString());
}

Hardware ID

HardwareId.Get() reads the Windows cryptography MachineGuid, /etc/machine-id on Linux, or the platform UUID on macOS, and falls back to the machine name. It does not use wmic, which was removed in Windows 11 24H2. Override it when you need a different binding policy:

var client = new PwfClient(new PwfClientOptions
{
    AppSecret    = secret,
    HardwareId   = myOwnFingerprint,
    BaseUrl      = "https://pwfauth.com",
});

Security note

The app secret ships inside your binary — that is inherent to the envelope protocol, which needs the key on the client to encrypt. Treat compiled output as sensitive: obfuscate release builds, and keep the secret out of public source control (read it from an environment variable or an encrypted config at startup). Anyone holding the secret can call the API as your application.

License

MIT © PWF Auth

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 105 8/2/2026
1.0.0 104 8/2/2026
1.0.0-preview.1 55 8/2/2026

Documentation only — no code change from 1.0.0. Adds a "What the login reply contains" section covering the licence fields returned by LoginAsync (expiry, days remaining, status, feature flags), with C# and VB.NET samples that handle the null expires_at of lifetime keys correctly.

1.0.0 — First stable release. License-key activation with hardware-ID binding and multi-device support, encrypted sessions over the AES-256-CBC + HMAC-SHA256 envelope, a server-driven kill switch (ban/pause/expire/HWID-reset/maintenance) with a consecutive-failure guard so an unreachable server ends the session instead of leaving the app running, free trials, remote texts/slides, and OTA update checks.