Skip to content

About

Official C# SDK for AuthForge — license key authentication with HMAC-verified heartbeats

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

37 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AuthForge C# SDK

Official C# SDK for AuthForge: credit-based license key authentication with Ed25519-verified responses.

Dependencies: BouncyCastle.Cryptography for Ed25519 verification. Targets .NET 6+.

How licensing works

  1. Activate: Login() calls POST /auth/validate once. The server checks revocation, expiry, HWID binding, and credits, then returns an Ed25519-signed session.
  2. Grace period (default): the app keeps running on that signed session without contacting AuthForge. The grace period equals the session TTL: default 24h, and the server clamps requests to between 1h and 7d. A background thread re-verifies the signed session locally and fails with session_expired once the grace period ends.
  3. Online check-ins (opt-in): pass onlineHeartbeat: true and the SDK instead calls POST /auth/heartbeat every heartbeatInterval seconds. Use this when you want fast revocation and concurrent-use detection instead of waiting for the grace period to lapse.

Features

Everything in this list ships in AuthForgeClient.cs today:

  • License validation via POST /auth/validate, returning a signed session.
  • Ed25519 signature verification on every /auth/validate and /auth/heartbeat response; tampered or unsigned responses are rejected.
  • Key rotation: a single-key constructor (also accepts a comma-separated string) and an IEnumerable<string> rotation-set constructor. The SDK trusts a signature that matches any key, so you can roll the server-side signing key without breaking deployed clients.
  • Nonce anti-replay: a fresh 128-bit nonce is sent on every request and the echoed nonce in the signed payload is checked before the response is accepted.
  • HWID: derived from the OS machine id (macOS IOPlatformUUID, Linux /etc/machine-id, Windows MachineGuid), identical across all AuthForge SDKs for the same app and machine.
  • hwidOverride: bind to any identity instead of the machine (for example tg:<id>, discord:<id>).
  • Seat enforcement: the server binds each HWID into a license's free slots up to maxHwidSlots; HwidCount / MaxHwidSlots are surfaced on the validate result. A shared (unlimited-seat) key skips per-device binding.
  • Grace period by default, online check-ins on demand (see Grace period and online check-ins).
  • Offline license files (.authforge): LoginFromFile() / VerifyLicenseFile() verify a cloud-minted, Ed25519-signed file with zero network access for air-gapped machines.
  • Self-serve offline activation: ActivateOffline() turns a license key into a lifetime, HWID-bound .authforge file with one online call, so a one-time-purchase app runs offline from then on.
  • Self-ban (SelfBan(...)) for anti-tamper response, both pre-session and post-session.
  • Grace period duration (ttlSeconds) with server-side clamping to [3600, 604800] (1h to 7d).
  • App variables / license variables for feature flags and tiered licensing.
  • Automatic retries for rate-limited and transient network failures, with a fresh nonce per retry.

Installation

The package is AuthForge on NuGet.

dotnet add package AuthForge

Alternative: copy AuthForgeClient.cs, AuthForgeClient.Offline.cs, AuthForgeClient.OfflineActivation.cs and AuthForgeException.cs into your solution if you need a source-only vendored layout (you still need the same NuGet dependencies declared in your project).

Quick Start

using AuthForge;

var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",          // from your AuthForge dashboard
    appSecret: "YOUR_APP_SECRET",  // from your AuthForge dashboard
    publicKey: "YOUR_PUBLIC_KEY"   // base64 Ed25519 public key from dashboard
);

Console.Write("Enter license key: ");
var key = Console.ReadLine() ?? "";

if (client.Login(key))
{
    Console.WriteLine("Authenticated!");
    // Your app logic here. The app activated online and now runs through
    // the grace period (default 24h) without contacting AuthForge.
}
else
{
    Console.WriteLine("Invalid license key.");
    Environment.Exit(1);
}

To enable online check-ins for fast revocation:

var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET",
    publicKey: "YOUR_PUBLIC_KEY",
    onlineHeartbeat: true,      // periodic /auth/heartbeat calls
    heartbeatInterval: 900      // seconds between check-ins
);

Configuration

Parameter Type Default Description
appId string required Your application ID from the AuthForge dashboard
appSecret string required for online APIs; "" for LoginFromFile only Your application secret from the AuthForge dashboard. Do not ship it in air-gapped binaries.
publicKey string / IEnumerable<string> required App Ed25519 public key(s) (base64) from dashboard. The single-string overload accepts a comma-separated trust list; the IEnumerable<string> overload takes a rotation set. The SDK trusts a signature matching any key (see Key rotation).
onlineHeartbeat bool false false: run through the grace period after activation, no network. true: enable online check-ins via /auth/heartbeat (see below)
heartbeatInterval int 900 Seconds between background checks (minimum 10; default 15 min). Applies to both the local grace period re-verification and online check-ins
apiBaseUrl string https://auth.authforge.cc API endpoint
onFailure Action<string, Exception?> null Callback on auth failure
requestTimeout int 15 HTTP request timeout in seconds
ttlSeconds int? null (server default: 86400, 24h) Requested grace period duration in seconds (the session token lifetime). Server clamps to [3600, 604800] (1h to 7d); preserved across online check-in refreshes.
hwidOverride string? null Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of the machine-derived HWID.

Identity-based binding example (Telegram/Discord)

var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET",
    publicKey: "YOUR_PUBLIC_KEY",
    onlineHeartbeat: true,
    hwidOverride: $"tg:{telegramUserId}" // or $"discord:{discordUserId}"
);

Key rotation

To rotate the server-side signing key without a flag-day, pass the new and previous keys via the IEnumerable<string> constructor; the SDK accepts a signature matching any entry:

var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET",
    publicKeys: new[] { "NEW_PUBLIC_KEY", "PREVIOUS_PUBLIC_KEY" },
    onlineHeartbeat: true
);

client.PublicKeys exposes the full trust list; client.PublicKey is the first (primary) entry. A comma-separated single string ("NEW,PREVIOUS") works too.

Grace period and online check-ins

Grace period (default): after one successful online activation, the SDK keeps the app running on the Ed25519-signed session with no further network calls. A background thread re-verifies the stored signature and the expiry timestamp every heartbeatInterval seconds; once the session TTL expires it triggers failure with session_expired. The grace period is session continuation within the session TTL (default 24h, clamped by the server to 1h through 7d), not persistent offline licensing: a mid-session revocation is only picked up at the next online validate.

Online check-ins (onlineHeartbeat: true): the SDK calls /auth/heartbeat every heartbeatInterval seconds with a fresh nonce, verifies signature + nonce, and triggers failure on invalid session state. Each successful check-in refreshes the session, so revocations and concurrent-use detection take effect on the next check-in instead of at the end of the grace period.

Offline license files (.authforge)

For machines that never connect to the internet, the operator mints a signed offline license file in the AuthForge dashboard (License page -> Mint .authforge file) or via POST /v1/licenses/{licenseKey}/offline-files. The file is a standalone Ed25519-signed document; the SDK verifies it with only your app public key and the machine HWID. It never contacts AuthForge and never starts the background thread. Pass an empty appSecret so the air-gapped binary does not contain the App Secret.

Grace period (default) Offline license file
Needs network Once, at Login() Never on the end machine
What is verified Signed session from /auth/validate Signed document minted in the cloud
Lifetime Session TTL: 1h to 7d Operator-chosen expiry or lifetime (perpetual licenses only)
Revocation Picked up at the next online validate / check-in Not reachable: the file stays valid until its own expiry
Cost 1 credit per Login() 1 credit per mint; verifying is free
var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",
    appSecret: "", // loginFromFile does not use the App Secret; do not ship it in air-gapped builds
    publicKey: "YOUR_PUBLIC_KEY",
    onFailure: (reason, ex) => Console.Error.WriteLine($"{reason}: {ex?.Message}"));

// 1. Write an activation request the operator drops into the mint dialog:
File.WriteAllText("machine.authforge-request", client.CreateActivationRequest());

// 2. Later, authorize from the minted file (path or armored text). No network.
if (client.LoginFromFile("license.authforge"))
{
    var info = client.GetOfflineLicense()!;
    Console.WriteLine($"Offline license OK until {info.ExpiresAt ?? "forever"}");
}

Since 2.0 every AuthForge SDK derives the same HWID for the same app and machine, so the HWID can come from any SDK build of the same app (the exception is the legacy fallback on machines without an OS machine id; see Hardware ID). HWIDs collected before 2.0 no longer match. After LoginFromFile(), GetSessionKind() returns SessionKind.Offline (SessionKind.Online after Login(), null when logged out).

AuthForgeClient.VerifyLicenseFile(text, appId, publicKeys, hwid) (static) and client.VerifyLicenseFile(pathOrText) perform the same checks without touching client state and return a VerifyLicenseFileResult (Ok, Error, License). Failure codes, in check order: bad_armor, bad_signature, unsupported_version, malformed_payload, wrong_app, expired, hwid_mismatch. LoginFromFile() reports them through onFailure("offline_login_failed", ex) and returns false; it never calls Environment.Exit.

File format (version 1): PEM-style armor with informational headers, a base64 JSON payload (v, appId, licenseKey, jti, kid, issuedAt, expiresAt, hwid policy, optional label/variable snapshots) and a detached Ed25519 signature over the UTF-8 bytes of the base64 payload string - the same contract as /auth/validate. See offline_license_vectors.json for conformance vectors.

Migrating from heartbeatMode

Earlier versions required a string heartbeatMode ("LOCAL" or "SERVER") as the 4th constructor parameter. The mapping:

  • heartbeatMode: "LOCAL" maps to the default grace period behavior: just remove the argument.
  • heartbeatMode: "SERVER" maps to onlineHeartbeat: true.

The old constructors still compile and behave exactly as before, but produce an obsolete warning (CS0618). The HeartbeatMode property is likewise obsolete; read OnlineHeartbeat instead.

// Before
new AuthForgeClient(appId, appSecret, publicKey, "LOCAL");
new AuthForgeClient(appId, appSecret, publicKey, "SERVER");

// After
new AuthForgeClient(appId, appSecret, publicKey);
new AuthForgeClient(appId, appSecret, publicKey, onlineHeartbeat: true);

Self-serve offline activation

For apps sold as a one-time purchase that should run offline after one online step. The end user enters their license key once; ActivateOffline(licenseKey, path) calls POST /auth/offline/activate, gets back a lifetime .authforge file bound to this machine's HWID, checks it locally, saves it to path and starts an offline session. Every later launch loads the file with LoginFromFile() and makes no network calls.

This is a separate mode from air-gapped offline files:

Air-gapped offline file Self-serve offline activation
Who mints the file The operator (dashboard / Developer API) The app, from the end user's license key
App Secret in the build No: pass "" Yes: required (it is an online call)
Network on the end machine Never Once per machine
Cost 1 credit per mint 2 credits per new machine, charged to the app owner; re-activating the same machine is free

The server only issues a file when the app has enabled self-serve offline activation, the license is perpetual (no expiry) and the license is seat-limited (not a shared key).

The app pattern: try the file first, activate only when there is no file or it does not verify (for example new hardware).

var licensePath = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp", "license.authforge");

var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET", // required for ActivateOffline
    publicKey: "YOUR_PUBLIC_KEY");

if (!(File.Exists(licensePath) && client.LoginFromFile(licensePath)))
{
    var key = PromptForLicenseKey(); // your UI
    try
    {
        client.ActivateOffline(key, licensePath); // throws AuthForgeException on refusal
    }
    catch (AuthForgeException ex)
    {
        var message = ex.Code switch
        {
            "offline_activation_limit_reached" => "This license is already activated on its maximum number of machines. Please contact the vendor.",
            "hwid_mismatch" => "All seats on this license are in use.",
            "invalid_key" => "That license key is not valid.",
            "revoked" or "expired" => "This license is no longer active.",
            "offline_activation_disabled" or "offline_activation_requires_perpetual" or "offline_activation_requires_seats"
                => "This license cannot be activated offline. Please contact the vendor.",
            _ when ex.IsTransient => "Could not complete activation. Check your connection and try again.",
            _ => $"Activation failed ({ex.Code}).",
        };
        ShowError(message);
        return;
    }
}
// Authenticated: GetSessionKind() == SessionKind.Offline, GetLicenseVariables(), GetOfflineLicense() ...

If you set onFailure, LoginFromFile() still reports a file that does not verify as onFailure("offline_login_failed", ex), so do not exit the app for that reason when you fall back to activation. ActivateOffline() itself never calls onFailure or Environment.Exit.

What ActivateOffline(string licenseKey, string? path = null) does:

  • Requires the App Secret. Without it, it throws the same ArgumentException as Login() before sending anything.
  • Sends appId, appSecret, licenseKey, the same hwid that Login() sends (including hwidOverride) and a fresh nonce, with the same retry policy as Login().
  • Verifies the returned file before writing anything: signature against your public key(s), app id, and this machine's HWID. A file that fails is refused with offline_file_rejected; the verifier's reason (bad_signature, wrong_app, hwid_mismatch, ...) is in ex.Message.
  • When path is set, writes the file byte-for-byte and atomically (temp file in the same directory, then rename), creating parent directories. On any failure an existing file at path is left untouched. With path omitted, nothing is written and only the session starts.
  • On success, returns true and leaves the client exactly as LoginFromFile() would: offline session, variables, GetOfflineLicense(), no background thread.
  • On failure, throws AuthForgeException and authenticates nothing: an existing session is left as it was. On success it replaces any current session, as LoginFromFile() does.

Errors (ex.Code):

Code Fatal Meaning
offline_activation_disabled yes The app has not enabled self-serve offline activation
offline_activation_requires_perpetual yes The license has an expiry; only perpetual licenses can self-serve
offline_activation_requires_seats yes Shared (unlimited-seat) licenses cannot self-serve
offline_activation_limit_reached yes Every machine slot on the license is used. HWID resets do not free them: tell the buyer to contact the vendor
hwid_mismatch yes No free seat for this machine
offline_file_rejected yes The returned file failed local verification; nothing was written
revoked, expired, blocked, invalid_app, app_disabled yes As for Login()
invalid_key, no_credits, rate_limited, system_error, network_error, timeout, unexpected_response, http_error_<status> no As for Login(); unexpected_response also covers a success response with no file
file_write_failed no The file could not be saved to path. The machine is activated, so calling again returns the same file for free

Billing and limits:

  • 1 new offline activation = 2 credits, charged to the app owner. A refused activation costs nothing.
  • Re-activating the same machine is free: a reinstall or a deleted file gets the identical file back.
  • A license can produce files for at most maxHwidSlots distinct machines, ever. HWID resets do not give machine slots back; the vendor can release a machine or raise the seat count.
  • A refund or chargeback revokes the license online but cannot reach files already issued. For a lifetime file, the buyer keeps the app.

Billing

  • One Login() or ValidateLicense() call = 1 credit (one /auth/validate debit each).
  • 10 online check-ins on the same session = 1 credit (debited on every 10th successful heartbeat). The default grace period behavior makes no heartbeat calls and costs nothing after activation.
  • 1 new offline activation (ActivateOffline()) = 2 credits; re-activating the same machine is free. LoginFromFile() costs nothing.

A desktop app running 6h/day with online check-ins at a 15-minute interval burns ~3-4 credits/day. /auth/heartbeat is limited to 6 requests/minute per license key, so keep intervals at 10 seconds or higher and choose cadence based on revocation speed needs (they always land on the next check-in).

Methods

Method Returns Description
Login(string licenseKey) bool Activates online and stores the signed session (sessionToken, expiresIn, appVariables, licenseVariables)
ValidateLicense(string licenseKey) ValidateLicenseResult Same /auth/validate + signatures as Login; does not update client session or start the background thread; never calls onFailure or Environment.Exit
SelfBan(...) Dictionary<string, object?> Requests /auth/selfban to blacklist HWID/IP and optionally revoke (session-authenticated only); throws on failure, including network failures; never calls onFailure or Environment.Exit
LoginFromFile(string pathOrText) bool Authorizes from an offline .authforge file with no network; never starts the background thread; failures go to onFailure("offline_login_failed", …)
ActivateOffline(string licenseKey, string? path = null) bool Self-serve offline activation: one online call returns a lifetime HWID-bound .authforge file, verified locally, saved atomically to path (optional), then an offline session as after LoginFromFile. Needs the App Secret; throws AuthForgeException on failure; never calls onFailure or Environment.Exit
VerifyLicenseFile(string pathOrText, DateTimeOffset? now = null) VerifyLicenseFileResult Verifies a .authforge file with this client's app id / keys / HWID without changing state
GetOfflineLicense() OfflineLicense? Metadata of the offline file in use (Jti, ExpiresAt, HwidPolicy, …)
GetSessionKind() SessionKind? SessionKind.Online, SessionKind.Offline, or null when logged out
GetHwid() string The HWID this client sends (or hwidOverride); customers share it to receive a bound file
CreateActivationRequest(ActivationRequestOptions? options = null) string Unsigned .authforge-request for this machine. No network, no secret. Hostname omitted unless IncludeMachineName
Logout() void Stops the background thread and clears all session/auth state
IsAuthenticated() bool True when an active authenticated session exists
GetSessionData() Dictionary<string, object?>? Full decoded payload map
GetAppVariables() Dictionary<string, object?>? App-scoped variables map
GetLicenseVariables() Dictionary<string, object?>? License-scoped variables map

Failure Handling

If authentication fails, the SDK calls your onFailure callback if one is provided. Without a callback, a transient background check failure (network outage, rate_limited, system_error, ...) writes a one-line warning to stderr and check-ins continue; every other failure (a failed Login, a definitive check-in answer, the grace period running out) calls Environment.Exit(1) to terminate the process, so your app cannot keep running without a valid license. Environment.Exit does not give your code a chance to save, so set onFailure if your app has anything to save.

A failed Login() calls onFailure("login_failed", ex) once and returns false. A server rejection arrives as an ArgumentException whose message is the server error code (e.g. invalid_key); a network failure (after the retry) arrives as an AuthForgeException with Code network_error or timeout.

ValidateLicense() returns a result object instead; it does not invoke onFailure or exit for validate/network failures.

SelfBan() throws instead: a server rejection as an ArgumentException whose message is the server error code, a network failure (after the retry) as an AuthForgeException with Code network_error or timeout. It never calls onFailure or Environment.Exit.

Recognized server errors: invalid_app, invalid_key, expired, revoked, hwid_mismatch, no_credits, app_burn_cap_reached, blocked, rate_limited, replay_detected, app_disabled, session_expired, revoke_requires_session, bad_request, malformed_request, demo_quota_exceeded, server_error, system_error, and for ActivateOffline() offline_activation_disabled, offline_activation_requires_perpetual, offline_activation_requires_seats, offline_activation_limit_reached. Codes added to the server later are passed through unchanged.

Request retries are automatic inside the internal HTTP layer:

  • rate_limited, or HTTP 429 with no error code: retry after 2s, then 5s (max 3 attempts total). HTTP 429 with no_credits, app_burn_cap_reached or demo_quota_exceeded is not retried.
  • network failure: retry once after 2s
  • every retry regenerates a fresh nonce

Background check failures

Background check failures reach onFailure("heartbeat_failed", ex), where ex is an AuthForgeException:

  • ex.Code: the server's error code from the response body, whatever the HTTP status, or an SDK code: network_error, timeout, http_error_<status> (non-JSON error body), invalid_json_response, unexpected_response, signature_mismatch, nonce_mismatch, .... For server codes ex.Message equals the code; network failures keep the url_error: ... message.
  • ex.IsTransient / ex.IsFatal: the classification. AuthForgeClient.IsTransientError(code) is the same check, and AuthForgeClient.DefinitiveErrorCodes lists the fatal codes.
Kind Codes What the SDK does
Fatal revoked, expired, hwid_mismatch (the HWID is no longer bound, for example after an HWID reset), blocked (HWID/IP blacklisted or not whitelisted), session_expired (also the grace period ending), malformed_request, app_disabled, invalid_app, signature_mismatch Clears the stored session (as Logout() does) and stops background checks before calling onFailure, so IsAuthenticated() is false. onFailure may call Login() again.
Transient Everything else: network_error, timeout, rate_limited, system_error, server_error, no_credits, demo_quota_exceeded, app_burn_cap_reached, bad_request, invalid_key, http_error_<status>, unexpected_response, unknown codes Keeps the session and checks in again on the next heartbeatInterval. Once the signed session's TTL has passed, the next transient failure is reported as a fatal session_expired instead.

DefinitiveErrorCodes also contains the self-serve offline activation codes (offline_activation_disabled, offline_activation_requires_perpetual, offline_activation_requires_seats, offline_activation_limit_reached, offline_file_rejected). Only ActivateOffline() produces them, never a check-in.

unexpected_response means a failed check-in body was not a well-formed AuthForge verdict ({"status":"failed","error":"<code>"}), for example a proxy or captive portal answering instead of AuthForge; the message includes the raw status and error values.

Transient failures only keep checking in if onFailure returns normally. Without a callback, a transient failure prints AuthForge: background check failed (<code>); retrying next interval to stderr and check-ins continue; a fatal one, including the session_expired a transient failure becomes once the session TTL has passed, still calls Environment.Exit(1). Network failures are reported once, as login_failed (from Login()) or heartbeat_failed with code network_error or timeout, not as a separate network_error reason.

To tolerate short outages but shut down on a definitive answer, have the callback signal your main thread and let the main thread save and exit:

using var licenseLost = new CancellationTokenSource();

var client = new AuthForgeClient(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET",
    publicKey: "YOUR_PUBLIC_KEY",
    onlineHeartbeat: true,
    ttlSeconds: 3600, // retry window for transient check-in failures
    onFailure: (reason, ex) =>
    {
        if (ex is AuthForgeException { IsTransient: true } transient)
        {
            // Connectivity problem or AuthForge overloaded: keep running. The SDK
            // checks in again every heartbeatInterval and reports session_expired
            // (fatal) once the ttlSeconds grace period is used up.
            Console.Error.WriteLine($"AuthForge check-in failed ({transient.Code}), retrying");
            return;
        }
        Console.Error.WriteLine($"License check failed: {reason} ({(ex as AuthForgeException)?.Code ?? ex?.Message})");
        // This can run on the heartbeat thread: signal the main thread instead of exiting here.
        licenseLost.Cancel();
    }
);

if (!client.Login(licenseKey))
{
    return 1;
}

while (!licenseLost.IsCancellationRequested)
{
    DoOneUnitOfWork(licenseLost.Token); // keep units short, or pass the token to async work
}

SaveUserWork();
client.Logout();
return 1;

In a WinForms or WPF app, marshal to the UI thread instead (form.BeginInvoke(...) or Dispatcher.InvokeAsync(...)) and close the main window normally. Calling Environment.Exit(1) inside onFailure is a last resort: finally blocks on other threads do not run and unsaved state is lost, so save the user's work first.

Thread safety: onFailure for heartbeat_failed runs on the background thread with no SDK lock held. Calling Logout(), IsAuthenticated() or Login() from it is safe; Logout() signals the thread to stop and never waits for it. A check-in response that arrives after Logout() or a new Login() is discarded, so it cannot restore the old session.

Self-ban (tamper response)

Use SelfBan(...) when anti-tamper checks trigger:

// Post-session (authenticated): defaults to revoke + HWID/IP blacklist.
client.SelfBan();

// Pre-session: pass licenseKey, SDK automatically disables revokeLicense.
client.SelfBan(licenseKey: "AF-XXXX-XXXX-XXXX");

// Custom flags:
client.SelfBan(
    blacklistHwid: true,
    blacklistIp: true,
    revokeLicense: false
);

SelfBan(...) auto-selects request mode:

  • Uses post-session mode when a session token is available (sessionToken argument or current SDK session).
  • Falls back to pre-session mode with licenseKey + nonce + app secret.
  • In pre-session mode, revoke is forced off client-side to avoid unsafe key revocations.
  • Not available after LoginFromFile(): offline sessions have no server session, so SelfBan() with no explicit licenseKey / sessionToken throws ArgumentException("offline_session") without contacting the server.
  • Failures throw: a server rejection as ArgumentException(<server error code>), a network failure as AuthForgeException (network_error / timeout). SelfBan() never calls onFailure or Environment.Exit, even without a callback.

How It Works

  1. Activate: Login uses hwidOverride if provided; otherwise it derives the HWID from the OS machine id (see Hardware ID). It then generates a random nonce and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.

  2. Background thread: a background thread wakes at the configured interval. With online check-ins enabled it sends a fresh nonce to /auth/heartbeat and verifies the response. Otherwise it re-verifies the stored signature and checks the grace period expiry without network calls.

  3. Crypto: both /validate and /heartbeat responses are signed by AuthForge with your app's Ed25519 private key. The SDK verifies every signed payload using your configured publicKey and rejects tampered responses.

Hardware ID

The HWID (v2, since 2.0) is

SHA256("authforge-hwid-v2|" + appId + "|" + machineId) as lowercase hex,

where machineId is the operating system's own machine id, trimmed and lowercased:

OS Source
macOS IOPlatformUUID (/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice)
Linux /etc/machine-id, then /var/lib/dbus/machine-id
Windows HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (64-bit registry view, via reg.exe /reg:64)

These ids survive network changes, renames, disk usage and USB devices. Including the app id means two different apps on the same machine get unrelated HWIDs. Every AuthForge SDK computes the same value for the same app id and machine; hwid_vectors.json pins this.

If no valid OS id can be read (for example a container without /etc/machine-id), the SDK falls back to legacy:<the pre-2.0 fingerprint of MAC, CPU and disk serial> as the machine id. Containers built from one image can also share a baked-in machine id, so for containers prefer hwidOverride with an identifier you control.

For non-device identities (for example Telegram users), pass hwidOverride such as tg:<user_id>.

Test Vectors

The test_vectors.json file is shared across all SDKs and validates cross-language Ed25519 verification behavior. offline_license_vectors.json (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for .authforge offline license files: good files plus the bad_signature, wrong key, wrong_app, expired, hwid_mismatch, unsupported_version and bad_armor rejects. self_serve_activation_vectors.json is the cross-SDK conformance suite for ActivateOffline(): mocked /auth/offline/activate responses with the expected outcome, written file, error code and classification. hwid_vectors.json (generated by authforge-node/generate_hwid_vectors.mjs) is the cross-SDK conformance suite for HWID v2: the derivation, machine id normalization, and the ioreg / reg.exe output parsers.

Requirements

  • .NET 6+
  • NuGet package: BouncyCastle.Cryptography

License

MIT

About

Official C# SDK for AuthForge — license key authentication with HMAC-verified heartbeats

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages