AtlasAuth

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

No other third-party libraries. The JSON parser/serializer the SDK needs is self-contained in atlasauth.cpp. Language standard: C++17.

bat
vcpkg install curl openssl

Build with MSVC, using the vcpkg toolchain. Link libcurl and OpenSSL's libcrypto:

bat
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.lib

With CMake plus the vcpkg toolchain file (-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmake):

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)
bash
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.

cpp
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...";

This is a public key. Never paste a private key. OpenSSL imports it via d2i_PUBKEY.

Construct the client with both:

cpp
atlas::Client client(APP_ID, PUBLIC_KEY);

3. Security model (what the SDK does for you)

Every signed response is this exact envelope:

json
{ "payload": "<compact-json-string>", "sig": "<base64 P1363 sig>" }

On each call the SDK:

  1. Makes a fresh cryptographically-random nonce (16 bytes turned into hex) in the

request body and holds it until the response is verified.

  1. Reads the envelope's payload member 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).

  1. base64-decodes sig into exactly 64 bytes (IEEE P1363 r||s), then converts

r||s to DER ECDSA_SIG and runs EVP_DigestVerify with EVP_sha256 over the exact payload bytes.

  1. Only if the signature is valid does it JSON-parse the payload for field access.
  2. Requires payload.nonce == the nonce this request sent (else it is replay/tamper, so

it fails).

  1. Advisory: requires abs(localUnix - payload.t) <= 60s of 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

cpp
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().

check can run. Defaults to empty (no version sent).

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.

cpp
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().

cpp
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.

cpp
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.

cpp
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()).

cpp
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".

cpp
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.

cpp
// 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)

GetterTypeMeaning
username()std::stringAuthenticated 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::stringHuman-readable last failure.
loggedIn()boolSession 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.

cpp
#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 repeatedly

stopHeartbeat() 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:

and lastError() describes the server's verdict. This is trustworthy.

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():

cpp
if (!client.login(user, pass)) {
    std::cerr << "Login refused / unverifiable: " << client.lastError() << "\n";
    return 1;
}

Per-call codes (from CONTRACT.md):

hwid_banned · ip_banned · license_expired · no_subscription · bad_input.

license_used · license_banned · hwid_banned · ip_banned · bad_input.

hwid_mismatch · hwid_banned · ip_banned · bad_input.

banned.

(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.

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.

cpp
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.

cpp
#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;
}