Admin panel β authorized access only
License key authentication system for desktop applications
CheeseAuth is a self-hosted license key validation backend. Any .exe or application can connect to it using a simple HTTP API to validate license keys, bind them to a machine (HWID), and maintain a live session.
app_secret.POST /api/authenticate with the user's key + their HWID + your app_secret. On success you receive a session token.POST /api/heartbeat every 30 seconds. If the heartbeat fails or the key gets banned, close the application.your-server.com:4000 with your actual server address throughout all examples.Go to Apps / Products in the admin panel β click + New App β enter a name (e.g. "MyTool v1") β copy the generated app_secret (format: ca_xxxxxxxxxxxxxxxx).
In the Generate New Key section, select your App from the dropdown, choose a duration, and click Generate. Distribute those keys to your users.
Your application needs to do 3 things:
Validates a license key and creates a session. On first use the key gets bound to the provided HWID. Works in two modes depending on whether app_secret is sent or not.
| Field | Type | Required | Description |
|---|---|---|---|
| key | string | β Always | The license key entered by the user (e.g. CHEESE-A1B2C3D4-E5F6A7B8) |
| hwid | string | β Always | Unique machine identifier β binds the key to this machine on first use |
| app_secret | string | βͺ Optional | Your app's secret (format: ca_...). Required only for direct C++ integration. The Universal Launcher does NOT send this. |
download_url.
{
"success": true,
"expiry": "12/31/2025",
"token": "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5",
"download_url": "http://your-server.com:4000/files/CheeseEsp.exe",
"version": "1.0.5",
"app_name": "CheeseEsp"
}
// download_url, version and app_name are null if the key has no app assigned
// or if the app has no file configured in Set Version.
{
"success": false,
"message": "Invalid license key"
}
| Message | Reason |
|---|---|
| Invalid license key | Key does not exist in database |
| This key has been banned | Manually banned or auto-banned by abuse detection |
| License expired | Past expiry date |
| Too many failed attempts for this key... | Per-key rate limit hit (5 failures / 10 min) |
| Key is already active on another device. | Concurrent session detected on different HWID |
| HWID mismatch. Reset required. | Key bound to different HWID |
| Key banned due to suspicious activity. | 10 HWID mismatches triggered auto-ban |
| Invalid app secret | Wrong app_secret (only when app_secret is sent) |
| This app is currently disabled | App was disabled from the admin panel |
Keeps the session alive. Must be called every 30 seconds after a successful authentication. Sessions expire after 90 seconds without a heartbeat.
| Field | Type | Description |
|---|---|---|
| token | string | The session token returned by /api/authenticate |
{ "success": true } // session still valid
{ "success": false } // session expired or key banned β EXIT APP
success: false for any reason, you must immediately close/exit the protected application. This is how the admin can remotely terminate sessions by banning a key.Uses WinINet for HTTP β no third-party libraries needed. Works on Windows only.
Add to your linker or at the top of your file:
#pragma comment(lib, "wininet.lib") #pragma comment(lib, "advapi32.lib") #include <windows.h> #include <wininet.h> #include <string>
#define SERVER_HOST "your-server.com" #define SERVER_PORT 4000 #define APP_SECRET "ca_your_secret_here"
std::string HttpPost(const char* path, const std::string& body) {
HINTERNET hInet = InternetOpenA("AuthClient", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hConn = InternetConnectA(hInet, SERVER_HOST, SERVER_PORT,
NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
HINTERNET hReq = HttpOpenRequestA(hConn, "POST", path, NULL, NULL, NULL, 0, 0);
const char* ct = "Content-Type: application/json\r\n";
HttpSendRequestA(hReq, ct, (DWORD)strlen(ct), (LPVOID)body.c_str(), (DWORD)body.size());
std::string resp; char buf[4096]; DWORD read;
while (InternetReadFile(hReq, buf, sizeof(buf)-1, &read) && read)
{ buf[read]=0; resp+=buf; }
InternetCloseHandle(hReq); InternetCloseHandle(hConn); InternetCloseHandle(hInet);
return resp;
}
std::string ParseField(const std::string& json, const std::string& key) {
std::string search = "\"" + key + "\":\"";
size_t s = json.find(search);
if (s == std::string::npos) return "";
s += search.size();
size_t e = json.find('"', s);
return (e != std::string::npos) ? json.substr(s, e - s) : "";
}
std::string g_token;
bool Authenticate(const std::string& key, const std::string& hwid,
std::string& outMsg, std::string& outExpiry) {
std::string body = "{\"key\":\"" + key + "\",\"hwid\":\"" + hwid +
"\",\"app_secret\":\"" APP_SECRET "\"}";
std::string resp = HttpPost("/api/authenticate", body);
if (resp.find("\"success\":true") == std::string::npos) {
outMsg = ParseField(resp, "message");
return false;
}
g_token = ParseField(resp, "token");
outExpiry = ParseField(resp, "expiry");
return true;
}
// Heartbeat thread β keeps session alive
DWORD WINAPI HeartbeatWorker(LPVOID) {
while (true) {
Sleep(30000);
std::string body = "{\"token\":\"" + g_token + "\"}";
std::string resp = HttpPost("/api/heartbeat", body);
if (resp.find("\"success\":true") == std::string::npos)
ExitProcess(1); // session expired or key banned
}
return 0;
}
// Usage in main():
// std::string msg, expiry;
// if (!Authenticate(userKey, hwid, msg, expiry)) { /* show error */ return 1; }
// CreateThread(NULL, 0, HeartbeatWorker, NULL, 0, NULL);
// // continue with your app...
Uses HttpClient (built into .NET 4.5+). No NuGet packages needed.
const string SERVER = "http://your-server.com:4000"; const string APP_SECRET = "ca_your_secret_here";
using System;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
static HttpClient http = new HttpClient();
static string sessionToken = "";
static async Task<bool> Authenticate(string key, string hwid) {
var payload = JsonSerializer.Serialize(new {
key, hwid, app_secret = APP_SECRET
});
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var resp = await http.PostAsync(SERVER + "/api/authenticate", content);
var json = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.GetProperty("success").GetBoolean()) {
sessionToken = root.GetProperty("token").GetString();
Console.WriteLine("Expires: " + root.GetProperty("expiry").GetString());
return true;
}
Console.WriteLine("Auth failed: " + root.GetProperty("message").GetString());
return false;
}
static async void StartHeartbeat() {
while (true) {
await Task.Delay(30000);
var payload = JsonSerializer.Serialize(new { token = sessionToken });
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var resp = await http.PostAsync(SERVER + "/api/heartbeat", content);
var json = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.GetProperty("success").GetBoolean())
Environment.Exit(1); // session expired
}
}
// Usage:
// if (!await Authenticate(userKey, hwid)) return;
// StartHeartbeat();
// // continue with your app...
Uses the requests library. Install with: pip install requests
SERVER = "http://your-server.com:4000" APP_SECRET = "ca_your_secret_here"
import requests
import threading
import sys
import time
SERVER = "http://your-server.com:4000"
APP_SECRET = "ca_your_secret_here"
session_token = None
def authenticate(key: str, hwid: str) -> bool:
global session_token
resp = requests.post(f"{SERVER}/api/authenticate", json={
"key": key,
"hwid": hwid,
"app_secret": APP_SECRET
}).json()
if resp.get("success"):
session_token = resp["token"]
print(f"Authenticated. Expires: {resp['expiry']}")
return True
print(f"Auth failed: {resp.get('message')}")
return False
def heartbeat_loop():
while True:
time.sleep(30)
try:
resp = requests.post(f"{SERVER}/api/heartbeat",
json={"token": session_token}).json()
if not resp.get("success"):
print("Session expired. Exiting.")
sys.exit(1)
except Exception:
sys.exit(1)
# Usage:
# hwid = get_hwid() # implement your own HWID function
# if not authenticate(user_key, hwid):
# sys.exit(1)
# t = threading.Thread(target=heartbeat_loop, daemon=True)
# t.start()
# # continue with your app...
HWID (Hardware ID) is what binds a license key to a specific machine. On first authentication the HWID gets saved to the key β afterwards only that machine can use it.
Store a randomly generated UUID in a hidden system file. This survives reboots and HWID spoofers, but is lost on Windows reinstall (handled by RST HWID in admin panel).
#define HWID_PATH "C:\\Windows\\System32\\spool\\drivers\\color\\wdc_sys.dat"
std::string GenerateUID() {
BYTE buf[16]; CryptGenRandom(CryptProvHdl, 16, buf); // get random bytes
char hex[33]; hex[32] = 0;
for (int i = 0; i < 16; i++) sprintf_s(hex+2*i, 3, "%02X", buf[i]);
return std::string(hex);
}
std::string GetHWID() {
HANDLE f = CreateFileA(HWID_PATH, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (f != INVALID_HANDLE_VALUE) {
char buf[64] = {}; DWORD read;
ReadFile(f, buf, 63, &read, NULL); CloseHandle(f);
if (read == 32) return std::string(buf, 32);
}
// File missing β generate new ID
std::string uid = GenerateUID();
HANDLE g = CreateFileA(HWID_PATH, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_HIDDEN|FILE_ATTRIBUTE_SYSTEM, NULL);
if (g != INVALID_HANDLE_VALUE) {
DWORD w; WriteFile(g, uid.c_str(), 32, &w, NULL); CloseHandle(g);
}
return uid;
}
Use the RST HWID button in the admin panel when a legitimate user reinstalls Windows and their HWID file is lost. This clears the bound HWID so the key can re-activate on the new installation.
Each app can have a version number and a linked .exe file. When your .exe starts, it checks the server for the latest version. If a newer version is available, it downloads and replaces itself automatically.
public/files/ folder on your Pterodactyl server.1.0.5) and the exact file name (e.g. cheese-aimbot.exe).{
"success": true,
"version": "1.0.5",
"download_url": "http://your-server.com:4000/files/cheese-aimbot.exe"
}
version from server != your hardcoded version β download download_url β replace current .exe β restart.#define APP_VERSION "1.0.3" // hardcoded in this build
#define APP_SECRET "ca_your_secret_here"
#define SERVER_HOST "your-server.com"
#define SERVER_PORT 4000
// Download file from URL to a local path using WinINet
bool DownloadFile(const std::string& url, const std::string& destPath) {
// Parse host and path from URL
// (simplified β assumes http://host:port/path format)
size_t hostStart = url.find("//") + 2;
size_t portPos = url.find(':', hostStart);
size_t pathStart = url.find('/', portPos);
std::string host = url.substr(hostStart, portPos - hostStart);
int port = std::stoi(url.substr(portPos + 1, pathStart - portPos - 1));
std::string path = url.substr(pathStart);
HINTERNET hInet = InternetOpenA("Updater", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hConn = InternetConnectA(hInet, host.c_str(), (INTERNET_PORT)port,
NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
HINTERNET hReq = HttpOpenRequestA(hConn, "GET", path.c_str(), NULL, NULL, NULL, 0, 0);
if (!HttpSendRequestA(hReq, NULL, 0, NULL, 0)) { InternetCloseHandle(hReq); return false; }
HANDLE hFile = CreateFileA(destPath.c_str(), GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) return false;
char buf[8192]; DWORD read;
while (InternetReadFile(hReq, buf, sizeof(buf), &read) && read) {
DWORD written; WriteFile(hFile, buf, read, &written, NULL);
}
CloseHandle(hFile);
InternetCloseHandle(hReq); InternetCloseHandle(hConn); InternetCloseHandle(hInet);
return true;
}
// Check version and auto-update if needed
// Returns false if update failed (caller should exit)
bool CheckAndUpdate() {
std::string url = "http://" SERVER_HOST ":" + std::to_string(SERVER_PORT) +
"/api/version?app_secret=" APP_SECRET;
// GET request (reuse HttpPost logic with GET method, or make a simple version):
HINTERNET hInet = InternetOpenA("Updater", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hUrl = InternetOpenUrlA(hInet, url.c_str(), NULL, 0, 0, 0);
if (!hUrl) { InternetCloseHandle(hInet); return true; } // skip update on network error
std::string resp; char buf[4096]; DWORD read;
while (InternetReadFile(hUrl, buf, sizeof(buf)-1, &read) && read) { buf[read]=0; resp+=buf; }
InternetCloseHandle(hUrl); InternetCloseHandle(hInet);
std::string serverVer = ParseField(resp, "version");
std::string dlUrl = ParseField(resp, "download_url");
if (serverVer.empty() || serverVer == APP_VERSION) return true; // up to date
// Newer version available β download to temp file then replace self
char selfPath[MAX_PATH], tempPath[MAX_PATH];
GetModuleFileNameA(NULL, selfPath, MAX_PATH);
std::string tempFile = std::string(selfPath) + ".old";
if (!DownloadFile(dlUrl, tempPath)) return true; // download failed, skip
// Schedule self-replace via cmd: rename old β .old, new β original name, restart
std::string cmd =
"cmd /c ping 127.0.0.1 -n 3 >nul"
" & move /y \"" + std::string(tempPath) + "\" \"" + std::string(selfPath) + "\""
" & start \"\" \"" + std::string(selfPath) + "\"";
ShellExecuteA(NULL, "open", "cmd.exe",
("/c " + cmd).c_str(), NULL, SW_HIDE);
ExitProcess(0); // exit current instance
return true;
}
// In main(), call BEFORE authenticate:
// CheckAndUpdate();
// ... then authenticate as normal
APP_VERSION. (2) Upload .exe to public/files/ on the server. (3) In admin panel, click β¬ Set Version on the app and enter the new version number. All users will auto-update on next launch.The Universal Launcher is a single CheeseLauncher.exe that works for ALL your products. It authenticates the user's key, automatically downloads the correct product exe, runs it from a hidden temp folder, and keeps the session alive via heartbeat β all without any code changes to your products.
| Step | Where |
|---|---|
| 1. Create App in admin panel | Apps section β + New App |
| 2. Generate keys assigned to that App | Generate section β select App from dropdown |
| 3. Upload your product .exe to the server | Pterodactyl β public/files/ |
| 4. Set Version in admin panel | Apps β β¬ Set Version β enter filename + version |
| 5. Distribute CheeseLauncher.exe to users | One universal launcher for all products |
public/files/ on Pterodactyl (replaces the old one)A user can have keys for multiple apps. The launcher saves each key separately and shows them all in the product dropdown.
// Example: user has 3 products SELECT PRODUCT βΌ CheeseEsp β key auto-filled, cached exe ready CheeseAimbot β key auto-filled, cached exe ready CheeseMisc β key auto-filled, cached exe ready οΌ Enter new key β add another product
π Any folder/ βββ CheeseLauncher.exe β the only file the user ever needs // Hidden in %APPDATA%\CheeseAuth\ (users never see this): // keys\CheeseEsp.key β saved key (DPAPI encrypted) // keys\CheeseAimbot.key β saved key (DPAPI encrypted) // cache\CheeseEsp\ β cached exe + version file // cache\CheeseAimbot\ β cached exe + version file
Before compiling the launcher, change these two lines in Program.cs:
// β Change SERVER to your actual domain or IP private const string SERVER = "http://cheese-auth.duckdns.org:4000"; private const string APP_VERSION = "1.0.0"; // version of the launcher itself
SERVER must point to your CheeseAuth server URL.download_url.| Task | Who handles it |
|---|---|
| HWID generation & binding | CheeseAuth.exe |
| Key validation | CheeseAuth.exe |
| Session token | CheeseAuth.exe |
| Heartbeat every 30s | CheeseAuth.exe |
| Auto-update & download | CheeseAuth.exe |
| Kill product exe if key is banned | CheeseAuth.exe |
Admin bans key in panel
β
Next heartbeat (max 30 seconds later) β server returns success: false
β
CheeseAuth.exe kills the product exe (Process.Kill)
β
CheeseAuth.exe exits
// The product exe is terminated automatically.
// The developer does not need to implement anything for this to work.
If someone wants to use the Universal Launcher with their product, they only need:
| What | Where to get it |
|---|---|
| CheeseAuth.exe (compiled launcher) | You provide this |
| App created in the admin panel | You create it, assign their exe as file_name |
| License key assigned to their App | You generate it from the panel |
public/files/ and you configure it in Set Version.All API endpoints return JSON. On error, success is false and message contains the reason.
| Code | Meaning |
|---|---|
| 200 | Request processed β check success field for result |
| 400 | Missing required fields or invalid parameters |
| 403 | Key banned, HWID mismatch, concurrent session, invalid app_secret, or app disabled |
| 404 | License key not found in database |
| 429 | Rate limit exceeded β IP (10/15min) or per-key (5 failures/10min) |
| 500 | Server-side database error |
| Message | HTTP | Reason |
|---|---|---|
| Missing key or HWID | 400 | Request body missing required fields |
| Invalid license key | 404 | Key does not exist in the database |
| This key has been banned | 403 | Key manually banned or auto-banned |
| Too many failed attempts for this key. Try again later. | 429 | Per-key rate limit: 5 HWID failures in 10 min |
| Key is already active on another device. | 403 | Concurrent session detected on a different HWID |
| HWID mismatch. Reset required. | 403 | Key is bound to a different HWID |
| Key banned due to suspicious activity. | 403 | 10 HWID mismatches triggered auto-ban |
| License expired | 403 | Key is past its expiry date |
| Invalid app secret | 403 | Wrong app_secret sent (C++ integration mode) |
| This app is currently disabled | 403 | App was disabled from the admin panel |
| Too many attempts. Try again in X minute(s). | 429 | IP rate limit: 10 attempts per 15 min |
| Database error | 500 | Server-side database failure |
| Rule | Limit | Result |
|---|---|---|
| IP login attempts | 10 per 15 minutes | IP temporarily blocked (429) |
| HWID mismatch per key | 5 per 10 minutes | Key temporarily blocked (429) |
| HWID mismatch per key | 10 total | Key auto-banned permanently (403) |
| Session without heartbeat | 90 seconds | Session expired, must re-authenticate |
| Heartbeat interval | Every 30 seconds | Session stays alive |
| App β | License Key β | Type β | Status β | HWID | Note β | Created β | Expires β | Last Login β | Actions |
|---|---|---|---|---|---|---|---|---|---|
| Loading⦠| |||||||||