PWFAuth 1.0.1
dotnet add package PWFAuth --version 1.0.1
NuGet\Install-Package PWFAuth -Version 1.0.1
<PackageReference Include="PWFAuth" Version="1.0.1" />
<PackageVersion Include="PWFAuth" Version="1.0.1" />
<PackageReference Include="PWFAuth" />
paket add PWFAuth --version 1.0.1
#r "nuget: PWFAuth, 1.0.1"
#:package PWFAuth@1.0.1
#addin nuget:?package=PWFAuth&version=1.0.1
#tool nuget:?package=PWFAuth&version=1.0.1
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
MaxHeartbeatFailuresbeats in a row (3 by default), the client ends the session itself withNETWORK_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 | Versions 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. |
-
.NETStandard 2.0
- System.Net.Http (>= 4.3.4)
- System.Text.Json (>= 8.0.5)
-
net8.0
- System.Text.Json (>= 8.0.5)
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.