AtlasAuth - C++ SDK Guide
The official AtlasAuth C++ SDK (atlasauth.h + atlasauth.cpp, C++17) 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 synchronous API under namespace atlas.
The SDK holds no secret. It only embeds your app_id (a UUID) and your app's
public key (SPKI DER, base64). A fake or man-in-the-middle server cannot forge a
valid response without the private key, which never leaves the AtlasAuth server. TLS
(peer plus host verification, HTTPS only) is enforced on every request.
1. Dependencies
- libcurl: HTTPS transport.
- OpenSSL: ECDSA verify, SHA-256, base64, secure random.
No other third-party libraries. The JSON parser/serializer the SDK needs is self-contained in atlasauth.cpp. Language standard: C++17.
Install & link - Windows (vcpkg)
vcpkg install curl opensslBuild with MSVC, using the vcpkg toolchain. Link libcurl and OpenSSL's libcrypto:
cl /std:c++17 /EHsc main.cpp atlasauth.cpp ^
/I"%VCPKG_ROOT%\installed\x64-windows\include" ^
/link /LIBPATH:"%VCPKG_ROOT%\installed\x64-windows\lib" libcurl.lib libcrypto.libWith CMake plus the vcpkg toolchain file (-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmake):
find_package(CURL REQUIRED)
find_package(OpenSSL REQUIRED)
add_executable(atlasdemo main.cpp atlasauth.cpp)
target_compile_features(atlasdemo PRIVATE cxx_std_17)
target_link_libraries(atlasdemo PRIVATE CURL::libcurl OpenSSL::Crypto)Install & link - Linux (apt)
sudo apt-get install libcurl4-openssl-dev libssl-dev
g++ -std=c++17 main.cpp atlasauth.cpp -lcurl -lcrypto -o atlasdemo(The same CMake snippet above works on Linux without the toolchain file.)
2. Configure your app id and public key
Paste both into the clearly-marked constants in your main.cpp (or wherever you construct the client). Both come from your AtlasAuth dashboard.
static const char* APP_ID =
"00000000-0000-0000-0000-000000000000"; // your app's UUID (not secret)
static const char* PUBLIC_KEY =
// SPKI DER public key, base64 (standard, padded).
// Base64 BODY ONLY - no "-----BEGIN PUBLIC KEY-----" header, no newlines.
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...";APP_ID: your app's UUID. Not secret.PUBLIC_KEY: your app's public key, SPKI DER then base64 (standard, padded).
This is a public key. Never paste a private key. OpenSSL imports it via d2i_PUBKEY.
Construct the client with both:
atlas::Client client(APP_ID, PUBLIC_KEY);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 cryptographically-random nonce (16 bytes turned into hex) in the
request body and holds it until the response is verified.
- Reads the envelope's
payloadmember as an opaque string, recovering the original
signed bytes by JSON-unescaping that string value only. It never re-serializes a parsed object (that would change byte order / spacing / escaping and break the signature).
- base64-decodes
siginto exactly 64 bytes (IEEE P1363r||s), then converts
r||s to DER ECDSA_SIG and runs EVP_DigestVerify with EVP_sha256 over the exact payload bytes.
- Only if the signature is valid does it JSON-parse the payload for field access.
- Requires
payload.nonce == the nonce this request sent(else it is replay/tamper, so
it fails).
- Advisory: requires
abs(localUnix - payload.t) <= 60sof skew.
Any non-verifiable response (bad signature, missing envelope, transport error, nonce mismatch) is treated as a failed / hostile call. The method sets lastError() and returns false / std::nullopt. A signed ok:false (wrong password, expired key, banned, and so on) is a signed truth. The method returns false and lastError() describes it.
4. Session lifecycle
atlas::Client client(APP_ID, PUBLIC_KEY);
client.init(); // required first
client.login(user, pass); // or register_(...) / licenseLogin(key)
client.startHeartbeat(onKicked); // background check() loop
// ... app runs ...
client.stopHeartbeat();
client.logout();All shared state is guarded by an internal mutex, so the heartbeat thread and your calling thread coexist safely. The getters take the lock and may be called from any thread.
5. Methods
void setVersion(std::string version) / void setHwidSeed(std::string seed)
Optional. Call before init()/login().
setVersionreports a client app version to/initso the server's forced-version
check can run. Defaults to empty (no version sent).
setHwidSeedswitches the HWID to the reproducible seed form (see §8).
bool init()
POST /init. Required before anything else. Returns false (and sets lastError()) if the app is not "active", a forced version mismatches, or the response can't be verified. After it succeeds, appStatus() and the heartbeat interval are known.
client.setVersion("1.0.0");
if (!client.init()) {
std::cerr << "init failed: " << client.lastError() << "\n";
return 1;
}
std::cout << "App status: " << client.appStatus() << "\n";bool login(const std::string& user, const std::string& pass)
POST /login. Takes a username and password (the HWID is computed and sent for you). Returns true only on a signed ok:true. Sets username() and expiryUnix().
if (!client.login(user, pass)) {
std::cerr << "Login failed: " << client.lastError() << "\n"; // invalid_credentials, hwid_mismatch, ...
return 1;
}
std::cout << "Welcome, " << client.username() << "\n";bool register_(const std::string& user, const std::string& pass, const std::string& license, const std::string& email = "")
POST /register. Self-signup that uses up a license key (only if your app allows it). The trailing underscore avoids the C++ keyword register. email is optional. The HWID is sent for you.
if (!client.register_("bob", "s3cret", "ATLAS-XXXX-YYYY", "bob@example.com")) {
std::cerr << "Register failed: " << client.lastError() << "\n"; // username_taken, invalid_license, ...
return 1;
}bool licenseLogin(const std::string& key)
POST /license. License-key-only login (no username or password). Sets expiryUnix() on success.
if (!client.licenseLogin("ATLAS-XXXX-YYYY")) {
std::cerr << "License failed: " << client.lastError() << "\n"; // invalid_license, license_expired, ...
return 1;
}std::optional<std::string> var(const std::string& name)
POST /var. Returns the variable value on signed ok:true && found:true, otherwise std::nullopt (auth-required, not found, or a verify failure; see lastError()).
if (auto v = client.var("welcome_message"))
std::cout << "Server message: " << *v << "\n";void log(const std::string& level, const std::string& message)
POST /log. Best-effort. It never throws and never meaningfully blocks. level must be "info", "warn", or "error".
client.log("info", "example client logged in");void startHeartbeat(std::function<void(std::string)> onKicked) / void stopHeartbeat()
See §6.
bool check(std::string& reason)
A single /check round-trip, exposed so you can poll manually instead of using startHeartbeat. Returns true if the session is still fully valid. On failure it sets reason (out) and lastError().
void logout()
POST /logout. Best-effort. It clears local auth state no matter what the network result is.
std::optional<StatusInfo> status() / std::vector<NewsItem> news()
These are public, unsigned informational reads (GET /api/v1/status/{app_id} and /api/v1/news/{app_id}). They send no nonce and no session and are not signature-verified, so treat them as display/monitoring data only and keep enforcement on the signed heartbeat. init() is not required. status() returns std::nullopt (and sets lastError()) if the app id is unknown. news() returns an empty vector.
// Live status banner - no login needed.
if (auto s = client.status())
std::cout << s->name << ": " << s->status << " - " << s->status_message
<< " (" << s->online << " online)\n";
// News / changelog feed.
for (const auto& item : client.news())
std::cout << (item.pinned ? "[pinned] " : "") << item.title << "\n"
<< item.body << "\n";StatusInfo holds app_id, name, status, status_message, online, and time. Each NewsItem holds id, title, body, pinned, created_at, and updated_at (unix seconds). News is ordered pinned-first then newest.
Getters (thread-safe)
| Getter | Type | Meaning |
|---|---|---|
username() | std::string | Authenticated username (if any). |
expiryUnix() | std::optional<int64_t> | Expiry as unix seconds. nullopt = lifetime / not set. |
appStatus() | std::string | "active" / "maintenance" / "disabled" ("unknown" pre-init). |
lastError() | std::string | Human-readable last failure. |
loggedIn() | bool | Session authenticated and not kicked. |
6. Heartbeat thread + onKicked
startHeartbeat spawns a background thread that calls /check every heartbeat seconds (the value learned from /init; defaults to 10). If a beat shows the session is no longer valid (ok==false, valid==false, app_status != "active", key_valid==false, or banned==true), onKicked is invoked once with the reason string and the loop stops. It is a no-op if it is already running or you are not authenticated.
#include <atomic>
#include <condition_variable>
#include <mutex>
std::mutex kickMtx;
std::condition_variable kickCv;
std::atomic<bool> kicked{false};
std::string kickReason;
client.startHeartbeat([&](std::string reason) {
{
std::lock_guard<std::mutex> lk(kickMtx);
kickReason = reason; // killed | expired | app_disabled | app_maintenance | banned | ...
kicked.store(true);
}
kickCv.notify_one();
});
// ... wait until kicked ...
{
std::unique_lock<std::mutex> lk(kickMtx);
kickCv.wait(lk, [&]{ return kicked.load(); });
}
client.stopHeartbeat(); // stops and JOINs the thread; safe to call repeatedlystopHeartbeat() stops and joins the thread and is safe to call multiple times. The callback runs on the heartbeat thread, so guard any shared state you touch from it (as above).
7. Error handling
There is one return convention and one error string:
- Signed
ok:false: the method returnsfalse(orvarreturnsstd::nullopt),
and lastError() describes the server's verdict. This is trustworthy.
- Unverifiable / hostile / transport: the method also returns
false/
std::nullopt and sets lastError(). This covers bad signature, 64-byte length mismatch, nonce mismatch, missing/malformed envelope, unsigned {error,code} bodies (unknown_app, rate_limited, server_error), and network/TLS errors. The SDK never trusts a non-verifiable response.
Always check the boolean and, on failure, surface lastError():
if (!client.login(user, pass)) {
std::cerr << "Login refused / unverifiable: " << client.lastError() << "\n";
return 1;
}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
(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): a SHA-256 hash of stable hardware identifiers, base64-encoded.
- Seed override: call
setHwidSeed(seed)(before init/login). The SDK then returns
base64(SHA256("atlasauth-hwid-seed|" + seed)), which is reproducible on any machine that knows the seed (so the legitimate owner can move their own key) and unguessable to others.
atlas::Client client(APP_ID, PUBLIC_KEY);
client.setHwidSeed("owner-portable-seed"); // before init()/login()
client.init();9. Complete main.cpp
Build and link as in §1. Fill in APP_ID and PUBLIC_KEY first.
#include "atlasauth.h"
#include <atomic>
#include <condition_variable>
#include <ctime>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
// ============================================================================
// >>> PASTE YOUR APP CREDENTIALS HERE <<<
// APP_ID : your app's UUID from the AtlasAuth dashboard.
// PUBLIC_KEY : your app's PUBLIC key, SPKI DER, base64 (standard, padded),
// body only (no "-----BEGIN PUBLIC KEY-----" header/newlines).
// ============================================================================
static const char* APP_ID =
"00000000-0000-0000-0000-000000000000";
static const char* PUBLIC_KEY =
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE_REPLACE_WITH_YOUR_APP_PUBLIC_KEY_";
int main() {
atlas::Client client(APP_ID, PUBLIC_KEY);
// Optional: report a client version (forced-version check) and/or a
// portable HWID seed. Both must be set BEFORE init()/login().
client.setVersion("1.0.0");
// client.setHwidSeed("my-portable-seed");
std::cout << "[AtlasAuth] Initializing...\n";
if (!client.init()) {
std::cerr << "init failed: " << client.lastError() << "\n";
return 1;
}
std::cout << "[AtlasAuth] App status: " << client.appStatus() << "\n";
// ---- Prompt for credentials ----
std::string user, pass;
std::cout << "Username: " << std::flush; std::getline(std::cin, user);
std::cout << "Password: " << std::flush; std::getline(std::cin, pass);
if (!client.login(user, pass)) {
std::cerr << "Login failed: " << client.lastError() << "\n";
return 1;
}
std::cout << "Login OK. Welcome, " << client.username() << ".\n";
if (auto exp = client.expiryUnix()) {
std::time_t t = static_cast<std::time_t>(*exp);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S UTC", std::gmtime(&t));
std::cout << "Subscription expires: " << buf << " (" << *exp << ")\n";
} else {
std::cout << "Subscription: lifetime (no expiry)\n";
}
// Optional: fetch an app variable (nullopt if missing / auth-gated).
if (auto v = client.var("welcome_message"))
std::cout << "Server message: " << *v << "\n";
// Best-effort log line.
client.log("info", "example client logged in");
// ---- Heartbeat: the server can kick us on any beat ----
std::mutex kickMtx;
std::condition_variable kickCv;
std::atomic<bool> kicked{false};
std::string kickReason;
client.startHeartbeat([&](std::string reason) {
{
std::lock_guard<std::mutex> lk(kickMtx);
kickReason = reason;
kicked.store(true);
}
kickCv.notify_one();
});
std::cout << "\nHeartbeat running. Press Enter to log out, "
"or wait to be kicked by the server.\n";
std::thread inputThread([&]() {
std::string line;
std::getline(std::cin, line);
{ std::lock_guard<std::mutex> lk(kickMtx); kicked.store(true); } // reason stays empty
kickCv.notify_one();
});
{
std::unique_lock<std::mutex> lk(kickMtx);
kickCv.wait(lk, [&]{ return kicked.load(); });
}
client.stopHeartbeat();
if (!kickReason.empty()) {
std::cout << "\nSigned out by the server. Reason: " << kickReason << "\n";
inputThread.detach(); // user may not have pressed Enter
} else {
std::cout << "Logging out...\n";
if (inputThread.joinable()) inputThread.join();
}
client.logout();
std::cout << "Done.\n";
return 0;
}