AtlasAuth - C# SDK Guide
The official AtlasAuth C# SDK is one file (AtlasAuth.cs) with no dependencies. It follows the frozen wire contract in CONTRACT.md (v1). It talks to https://atlasauth.cc/api/v1, checks the server's ECDSA P-256 / SHA-256 signature on every security-relevant response, and gives you a small async API.
The SDK holds no secret. It only embeds your app_id (a UUID) and your app's
public key (SPKI DER, base64). Shipping both in your binary is safe and required.
A fake or man-in-the-middle server cannot forge a valid response without the private
key, which never leaves the AtlasAuth server.
1. Requirements & install
- .NET 6+ (also fine on .NET Core 3.0+ / .NET 5+). These runtimes expose
DSASignatureFormat.IeeeP1363FixedFieldConcatenation, which lets the SDK verify the raw 64-byte IEEE P1363 r||s signature directly, with no DER conversion. On legacy .NET Framework 4.x there is no DSASignatureFormat overload, so you would need BouncyCastle (Org.BouncyCastle.Crypto.Signers.ECDsaSigner) to verify the same payload bytes. The shipped file targets .NET 6+ on purpose and takes no third-party dependency.
- Dependencies: only the BCL:
System.Security.Cryptography,System.Net.Http,
System.Text.Json, System.Text, Microsoft.Win32 (registry, for the Windows HWID). No System.Management/WMI, no NuGet packages.
To install, just drop AtlasAuth.cs into your project. That's it.
dotnet new console -o atlasdemo
# copy AtlasAuth.cs into atlasdemo/
# copy this guide's Program.cs into atlasdemo/ (replacing the generated one)
cd atlasdemo
dotnet runThe public namespace is AtlasAuth. The client type is AtlasAuth.AtlasClient.
2. Configure your app id and public key
You have two choices. Hard-code the two constants at the top of AtlasAuth.cs, or pass the values to the constructor (a non-empty constructor argument wins). Both come from your AtlasAuth dashboard.
// Inside AtlasAuth.cs (the clearly-marked constants):
public const string APP_ID = "00000000-0000-0000-0000-000000000000";
public const string PUBLIC_KEY = "REPLACE_WITH_BASE64_SPKI_PUBLIC_KEY";APP_ID: your app's UUID. Not secret.PUBLIC_KEY: your app's public key, SPKI DER encoded then base64 (the base64
body only, with *no* -----BEGIN PUBLIC KEY----- header and no newlines). This is a public key. Never paste a private key here.
The constructor fails fast. It throws AtlasAuthException if APP_ID is still the all-zero placeholder, if PUBLIC_KEY is the placeholder or is not valid base64, or if the bytes do not import as an SPKI DER ECDSA public key (ImportSubjectPublicKeyInfo).
using var client = new AtlasClient(APP_ID, PUBLIC_KEY);
// Optional 3rd arg: an HWID seed (see §8):
// using var client = new AtlasClient(APP_ID, PUBLIC_KEY, "my-secret-seed");3. Security model (what the SDK does for you)
Every signed response is this exact envelope:
{ "payload": "<compact-json-string>", "sig": "<base64-p1363-sig>" }On each call the SDK:
- Makes a fresh random nonce (16 bytes turned into lowercase hex) and puts it in the
request body. It holds the nonce until the matching response is verified.
- Reads
payloadas an opaque UTF-8 string. It never re-serializes it (the
signature is over those exact bytes).
- base64-decodes
siginto exactly 64 bytes (IEEE P1363r||s, not DER). - Verifies
ECDSA-P256-SHA256(sig, utf8(payload), PUBLIC_KEY). If invalid, it throws
AtlasAuthException (hostile; the payload is never parsed).
- Parses the payload, then requires
payload.nonce == the nonce this request sent
(else it is replay/tamper, so it throws).
- Advisory: checks
abs(localUnix - payload.t) <= 60s. On drift it records
LastCode = "clock_skew" but does not hard-fail.
A signed ok:false (wrong password, expired key, banned, and so on) is a signed truth. The method returns false and you read LastError / LastCode. Only unverifiable results (bad signature, nonce mismatch, transport-level unsigned {error,code}) throw AtlasAuthException.
4. Session lifecycle
Init() -> Login() | Register() | License() -> StartHeartbeat(onKicked) -> Logout()Init() must succeed before anything else. All API methods are async and accept an optional CancellationToken.
5. Methods
Task<bool> Init(string? clientVersion = null, CancellationToken ct = default)
POST /init. Sets up the session token and reads app status plus the heartbeat interval. Returns true only if a session was established and AppStatus == "active" and the forced-version check (if any) passes. On a non-active app or version mismatch it returns false and sets LastError/LastCode ("maintenance", "disabled", or "version_mismatch"). Throws on verify/nonce/transport failure.
if (!await client.Init(clientVersion: "1.0.0"))
{
Console.Error.WriteLine($"Init failed: {client.LastError} [{client.LastCode}]");
return;
}
Console.WriteLine($"Connected to '{client.AppName}' " +
$"(status: {client.AppStatus}, heartbeat: {client.HeartbeatSeconds}s).");Task<bool> Login(string username, string password, CancellationToken ct = default)
POST /login. Takes a username and password (the HWID is computed and sent for you). On success it sets Username, Expiry, RemainingSeconds, and Level.
if (await client.Login("alice", "hunter2"))
Console.WriteLine($"Welcome, {client.Username}! Level {client.Level}");
else
Console.WriteLine($"Login failed: {client.LastCode}"); // invalid_credentials, hwid_mismatch, ...Task<bool> Register(string username, string password, string license, string? email = null, CancellationToken ct = default)
POST /register. Self-signup that uses up a license key (only if your app allows registration). email is optional. The HWID is sent for you.
if (await client.Register("bob", "s3cret", "ATLAS-XXXX-YYYY", email: "bob@example.com"))
Console.WriteLine($"Registered as {client.Username}, expiry {client.Expiry}");
else
Console.WriteLine($"Register failed: {client.LastCode}"); // username_taken, invalid_license, ...Task<bool> License(string key, CancellationToken ct = default)
POST /license. License-key-only login (no username or password). Sets Expiry, RemainingSeconds, and Level on success.
if (await client.License("ATLAS-XXXX-YYYY"))
Console.WriteLine(client.Expiry is null ? "Lifetime key" : $"Expires {client.Expiry}");
else
Console.WriteLine($"License failed: {client.LastCode}"); // invalid_license, license_expired, ...Task<string?> Var(string name, CancellationToken ct = default)
POST /var. Fetches an app variable by name. Returns the string value, or null if it is not found or not permitted. In that case LastCode is set (for example "auth_required" for an auth-gated variable fetched on an unauthenticated session, or "not_found").
string? motd = await client.Var("motd");
if (motd != null) Console.WriteLine($"MOTD: {motd}");
else if (client.LastCode == "auth_required") Console.WriteLine("That variable needs login.");Task Log(string level, string message, CancellationToken ct = default)
POST /log. Best-effort client-side logging (app log plus an optional Discord webhook). It never throws on a signed response, and transport failures are swallowed so logging can't break your app. level must be "info", "warn", or "error" (anything else is coerced to "info"). It is a no-op if Init() hasn't run.
await client.Log("info", "user opened the loader");
await client.Log("error", "something went wrong in module X");void StartHeartbeat(Action<string> onKicked) / void StopHeartbeat()
See §6.
Task Logout(CancellationToken ct = default)
POST /logout. Stops the heartbeat first, ends the session server-side (best-effort), and clears local auth state (Username, Expiry, RemainingSeconds, Level, LoggedIn). Safe to call multiple times.
await client.Logout();Task<StatusInfo?> Status(CancellationToken ct = default) / Task<List<NewsItem>> News(CancellationToken ct = default)
These are public, unsigned informational reads (GET /api/v1/status/{app_id} and /api/v1/news/{app_id}). They take no session, send no nonce, and are not signature-verified. Treat them as display/monitoring data only, and keep enforcement on the signed heartbeat. No Init() is required. Status() returns null and sets LastCode = "unknown_app" if the app id is unknown. News() returns an empty list.
// Live status banner (e.g. on a launcher splash) - no login needed.
var s = await client.Status();
if (s != null)
Console.WriteLine($"{s.Name}: {s.Status} - {s.StatusMessage} ({s.Online} online)");
// News / changelog feed.
foreach (var item in await client.News())
Console.WriteLine($"{(item.Pinned ? "[pinned] " : "")}{item.Title}\n{item.Body}");StatusInfo carries AppId, Name, Status, StatusMessage, Online, and Time. Each NewsItem carries Id, Title, Body, Pinned, CreatedAt, and UpdatedAt (unix seconds). News is ordered pinned-first then newest.
Properties
| Property | Type | Meaning |
|---|---|---|
Username | string? | Authenticated username (after login/register), else null. |
Expiry | DateTimeOffset? | Key/subscription expiry. null = lifetime (or not yet known). |
AppStatus | string? | Last observed "active" / "maintenance" / "disabled" (null pre-init). |
LastError | string? | Human-readable last error. |
LastCode | string? | Machine-readable last code (e.g. invalid_credentials, rate_limited). |
LoggedIn | bool | True after a successful login / register / license. |
Initialized | bool | True once Init() succeeded and a session token is held. |
HeartbeatSeconds | int | Server-dictated heartbeat interval from /init (default 10). |
AppName | string? | App display name from /init. |
StatusMessage | string? | Message to show the user when the app is not active. |
LatestVersion | string? | Latest app version advertised by /init. |
RemainingSeconds | long? | Remaining subscription seconds, if reported. |
Level | int? | User level from /login, if reported. |
6. Heartbeat + onKicked
After you authenticate, start the background heartbeat. It calls /check every HeartbeatSeconds seconds. The moment a signed reply says the session is no longer valid (ok==false, valid==false, app_status != "active", key_valid==false, or banned==true), it calls your onKicked(reason) callback exactly once and stops. It also kicks (and reports) if a heartbeat can't be verified, since an unverifiable beat is treated as hostile (reason will be "verify_failed").
using var exit = new ManualResetEventSlim(false);
client.StartHeartbeat(reason =>
{
// Invoked on a background thread - marshal to your UI thread if needed.
// reason ∈ killed | expired | app_disabled | app_maintenance | banned | verify_failed | ...
Console.Error.WriteLine($"Session ended by server: {reason}. Exiting.");
exit.Set();
});
exit.Wait(); // block until kicked (or your own signal)
client.StopHeartbeat();Notes:
- A transient network error on a single beat does not kick you. It is recorded in
LastError and retried on the next beat. Only a verified "no longer valid" reply or an unverifiable beat triggers onKicked.
- The callback runs on a background thread, and the library never writes to the console.
StopHeartbeat()is safe to call any time.Logout()calls it for you.
7. Error handling
There are two separate failure channels:
- Signed
ok:false: the method returnsfalse(orVarreturnsnull). Read
LastError (human) and LastCode (machine). This is a trustworthy server verdict.
- Unverifiable / hostile / transport: the method throws
AtlasAuthException. This
covers bad signature, 64-byte length mismatch, nonce mismatch, malformed envelope, unsigned {error,code} bodies (e.g. unknown_app, rate_limited, server_error), timeouts, and network errors. ex.Code carries the machine code when available.
try
{
if (!await client.Login(user, pass))
{
// signed verdict
Console.Error.WriteLine($"Login refused: {client.LastError} [{client.LastCode}]");
return;
}
}
catch (AtlasAuthException ex)
{
// could not trust the server's answer - do NOT proceed
Console.Error.WriteLine($"Cannot trust server: {ex.Message} [{ex.Code}]");
return;
}Per-call codes (from CONTRACT.md):
/login:ok·invalid_credentials·user_banned·hwid_mismatch·
hwid_banned · ip_banned · license_expired · no_subscription · bad_input.
/register:ok·register_disabled·username_taken·invalid_license·
license_used · license_banned · hwid_banned · ip_banned · bad_input.
/license:ok·invalid_license·license_expired·license_banned·
hwid_mismatch · hwid_banned · ip_banned · bad_input.
/checkkick reasons:killed·expired·app_disabled·app_maintenance·
banned.
- Transport (unsigned, 4xx/5xx):
unknown_app·bad_request·rate_limited(with a
Retry-After header) · server_error.
8. HWID + seed override
The SDK computes an opaque hwid and sends it on /login, /register, and /license. The server binds the first hwid it sees for a key/user and then requires an exact match.
- Default (no seed):
base64(SHA256(join("|", machineIds))), where the ids are the
Windows registry MachineGuid (HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid) plus Environment.MachineName plus Environment.ProcessorCount.
- Seed override: if you pass a seed to the constructor, the SDK instead returns
base64(SHA256("atlasauth-hwid-seed|" + seed)). This is reproducible on any machine that knows the seed (so the legitimate owner can move their own key) and is unguessable to others. Set it at construction, before Init/Login.
using var client = new AtlasClient(APP_ID, PUBLIC_KEY, hwidSeed: "owner-portable-seed");
string id = client.ComputeHwid(); // inspect the value if you likeStronger fingerprint hook: to bind to more hardware (motherboard/disk/MAC serials), gather those strings yourself and feed them through the static AtlasClient.HashIdsToHwid(IEnumerable<string>), which produces the exact canonical base64(SHA256(join("|", ids))) form. Keep the same inputs across runs, or the user will be locked out. Do not pull in System.Management/WMI unless you accept that dependency.
9. Complete runnable Program.cs
Drop this beside AtlasAuth.cs (delete the auto-generated Program.cs), fill in APP_ID and PUBLIC_KEY, then run dotnet run.
using System;
using System.Threading;
using System.Threading.Tasks;
using AtlasAuth;
internal static class Program
{
// -------- FILL THESE IN (from the AtlasAuth dashboard) --------------------
// APP_ID is a UUID. PUBLIC_KEY is base64 of the SPKI DER public key
// (the base64 body only - no "-----BEGIN PUBLIC KEY-----" header).
private const string APP_ID = "00000000-0000-0000-0000-000000000000";
private const string PUBLIC_KEY = "REPLACE_WITH_BASE64_SPKI_PUBLIC_KEY";
// -------------------------------------------------------------------------
private static async Task<int> Main()
{
// Optional 3rd arg = HWID seed for a portable key:
// new AtlasClient(APP_ID, PUBLIC_KEY, "my-secret-seed");
using var client = new AtlasClient(APP_ID, PUBLIC_KEY);
// ---- 1) init -------------------------------------------------------
Console.WriteLine("Connecting to AtlasAuth...");
try
{
if (!await client.Init(clientVersion: "1.0.0"))
{
Console.Error.WriteLine($"Init failed: {client.LastError} [{client.LastCode}]");
return 1;
}
}
catch (AtlasAuthException ex)
{
// Thrown on signature/nonce/transport failure - treat as hostile.
Console.Error.WriteLine($"Cannot trust server: {ex.Message}");
return 1;
}
Console.WriteLine($"Connected to '{client.AppName}' " +
$"(status: {client.AppStatus}, heartbeat: {client.HeartbeatSeconds}s).");
// ---- 2) login ------------------------------------------------------
Console.Write("Username: ");
string user = Console.ReadLine()?.Trim() ?? "";
Console.Write("Password: ");
string pass = Console.ReadLine() ?? "";
bool ok;
try
{
ok = await client.Login(user, pass);
}
catch (AtlasAuthException ex)
{
Console.Error.WriteLine($"Login could not be verified: {ex.Message}");
return 1;
}
if (!ok)
{
Console.Error.WriteLine($"Login failed: {client.LastError} [{client.LastCode}]");
return 1;
}
Console.WriteLine($"Welcome, {client.Username}!");
Console.WriteLine(client.Expiry is { } exp
? $"Subscription expires: {exp.LocalDateTime} " +
$"(in {client.RemainingSeconds?.ToString() ?? "?"}s)"
: "Subscription: lifetime");
// Optional: read an app variable.
try
{
string? motd = await client.Var("motd");
if (motd != null) Console.WriteLine($"MOTD: {motd}");
}
catch (AtlasAuthException) { /* ignore optional var */ }
// Best-effort log line.
await client.Log("info", "example client logged in");
// ---- 3) heartbeat --------------------------------------------------
using var exit = new ManualResetEventSlim(false);
client.StartHeartbeat(reason =>
{
Console.Error.WriteLine($"\nSession ended by server: {reason}. Exiting.");
exit.Set();
});
Console.WriteLine("Running. Press Ctrl+C to quit, " +
"or the app exits if the session is revoked.");
Console.CancelKeyPress += (_, e) => { e.Cancel = true; exit.Set(); };
exit.Wait(); // block until kicked or Ctrl+C
client.StopHeartbeat();
await client.Logout();
return 0;
}
}