ArcticAuth

C# / .NET client

Validating a key from a desktop, service or game client, and what a licence check can and cannot buy you there.

Enter jumps to the next match, shift-enter the previous.

Download documentation

One Markdown file covering the whole product. Last changed August 30, 2026.

Scope

Live: everything on this page. Key validation, hardware binding, the trust list verdicts and the key page all answer today.

Not this client: the secure vault. Script delivery is Roblox-only - it exists because an executor can be handed bytes and a desktop app already has its own on disk. A .NET app validates keys; it does not receive its program from us.

C++: this is a managed assembly, so a native application cannot load it directly - there is no C entry point to call. Either host it through C++/CLI on Windows, or talk to POST /api/validation/v1/validate yourself: it is one JSON request and needs no library at all.

Getting the library

One assembly, no native dependencies. It multi-targets netstandard2.0 - which covers .NET Framework 4.6.1 and up, Unity and Mono - and net8.0 for modern hosts, so a reference resolves to the right one on its own.

Download the DLLGoogle Drive ยท both target frameworks
build from source
dotnet build src/ArcticAuth.Client/ArcticAuth.Client.csproj -c Release

# src/ArcticAuth.Client/bin/Release/netstandard2.0/ArcticAuth.Client.dll
# src/ArcticAuth.Client/bin/Release/net8.0/ArcticAuth.Client.dll

Builds are deterministic: the same source produces the same bytes. That is what makes the download above checkable - build the project yourself and compare the SHA-256 against the file you got. If they match, the assembly you are about to ship is the one we published, and you did not have to trust the link to find that out.

Quick start

Three lines to a verdict. The service id is not a secret - it is already in the key page URL your users visit - so shipping it inside your binary is not a leak.

Program.cs
using ArcticAuth.Client;

using var client = new ArcticAuthClient("your-service-id");

var result = await client.ValidateAsync(userEnteredKey);

if (!result.IsValid)
{
    Console.WriteLine(result.Message ?? result.Reason.ToString());
    Console.WriteLine($"Get a key: {client.GetKeyUrl()}");
    return;
}

// Valid. Now go and fetch something only a valid session can get.

ArcticAuthClient

new ArcticAuthClient(serviceId, apiBaseUrl?, httpClient?)
Binds a client to one service. Pass your own HttpClient to share a connection pool or install a handler - when you do, its lifetime stays yours and this will not dispose it. apiBaseUrl is for a self-hosted deployment.
ValidateAsync(key, ct) -> Task<ValidationResult>
Checks a key against this service and this machine. Never throws for an expected condition: no network, a timeout, a malformed reply and a refused key all come back as a result, because the caller has to handle "could not check" either way.
PingAsync(ct) -> Task<bool>
Whether the API is answering. Lets you tell "the service is down" apart from "my key is bad" without spending a validation.
GetKeyUrl(keyPageBaseUrl?) -> string
The key page for this service with the hardware id already attached, so the page can bind a key to this machine without the user typing anything.
HardwareIdentifier -> string
The machine id this client reports. Show it on a support screen: a wrong-machine refusal is about this exact string, and having the user read it out beats guessing.

The default endpoint is https://api.arcticauth.com/api/validation/v1.IDisposable, so using it is the normal shape.

Reading the result

IsValid -> bool
Whether the server accepted the key. Read the warning below before building anything important on it.
Reason -> ValidationReason
Why it was refused. None when it was not.
Message -> string?
The server's own wording, when it sent any. Safe to show a user.
ExpiresAt -> DateTimeOffset?
When the key stops working, if the server said.
IsTransient -> bool
Whether retrying later might succeed. True for Unreachable, RateLimited and BadResponse; false for a key that is simply bad. This is the flag to branch on for a retry, not the reason.

Do not gate a feature on IsValid alone. The result reports what the server said; it does not enforce it. Anyone running your software can patch the call site to ignore the answer, and no obfuscation, packing or anti-debug in this assembly changes that.

Gate on something the server only hands over to a valid session - a download, a signed token, a computation done server-side. See Protecting a desktop client below.

Refusal reasons

A closed enum, so you can switch on it. An unrecognised string from the server maps to Unknown rather than throwing: a server that adds a reason must not break clients built before it existed.

MissingKey
No key was supplied.
InvalidKey
No such key, or it does not belong to this service.
Expired
The key exists and its window has passed.
Revoked
Pulled by the service owner.
HardwareMismatch
Valid key, different machine. It belongs to the one that redeemed it first.
MissingHardwareId
The server needed a machine to bind to, or to rule on against the trust list, and the request named none.
Blocked
The owner put this machine on the trust list as blocked.
NotTrusted
The service runs an allow-only trust list and this machine is not on it. Nobody banned them; they were never invited.
ServicePaused
The service exists and its owner has taken it offline.
NoService
No such service. Check the id you compiled in.
RateLimited
Too many attempts from here. Back off and retry.
Unreachable
The request never completed: no network, DNS, TLS, timeout.
BadResponse
A reply arrived and could not be understood.
Unknown
The server said something this client has no name for.
handling the ones users can act on
switch (result.Reason)
{
    case ValidationReason.HardwareMismatch:
        Show($"That key belongs to another machine. This one is {client.HardwareIdentifier}.");
        break;

    case ValidationReason.Expired:
    case ValidationReason.Revoked:
    case ValidationReason.InvalidKey:
        Show($"Get a key: {client.GetKeyUrl()}");
        break;

    default:
        // Transient: the wifi dropped or we are being rate limited. Not their fault,
        // and not a reason to wipe a stored key.
        if (result.IsTransient) ScheduleRetry();
        break;
}

Show the reason to the user. Every one of these is something they can act on, and a bare "invalid licence" turns a wrong-machine problem into a support ticket.

Hardware ID

A SHA-256 over the machine name, user name, OS platform and processor count, hashed before it leaves. The raw inputs are personal data the server does not need - it only needs to know whether two runs came from the same place.

Deliberately not a disk serial or a MAC address. Those need privileged calls or platform-specific APIs, and they change when hardware is swapped or a VPN adapter appears. A user whose id moved because they plugged in a dock costs more in support than the precision was worth.

It is also per-user, not just per-machine: two accounts on one PC report different ids. And it is a claim, computed on the caller's machine - anyone with a debugger can return whatever they like. The server binds a key to the first value it sees and refuses the rest, which makes this a sharing control, not an anti-tamper one.

Protecting a desktop client

Worth being blunt, because this is the part that differs most from the Roblox vault. There, a failed check returns no script bytes, so there is nothing on the machine to patch. Here your whole program is already on the attacker's disk before the first check runs.

A licence check in a shipped binary can always be removed. One byte flipped in a branch, or one method stubbed in a decompiler, and IsValid is true forever. This assembly ships unobfuscated and decompiles cleanly, which changes nothing about that - it carries no credentials, the endpoint is public, and the request is four fields of JSON.

What actually works is moving the value to where the patch cannot reach:

  • Ship the feature, not the flag. Have the server return the thing a paid user came for - the model, the data, the licence file, the computed answer - on a validated session. Patching the check then yields a program with nothing to show.
  • Validate on use, not only on start. A check at launch is one branch to find. A check bound to each server round trip is not a branch at all.
  • Treat transient failures as transient. Wiping a stored key because the wifi dropped is the single most common way a licence system loses a paying customer. Branch on IsTransient.

The repository's PROTECTION.md goes through this at length, with the threat model spelled out.