AtlasAuth

AtlasAuth - HTTP API Reference (v1)

This is the raw HTTP reference for callers who are not using an official SDK. It restates the frozen wire contract (CONTRACT.md, v1) directly. The SDKs implement exactly this. If you build your own client, you must verify signatures and nonces exactly as described here. Treat any response you cannot verify as a failed, hostile call.


1. The signed envelope

Every security-relevant response has HTTP 200 and exactly this body:

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

payload is a JSON string whose value is itself compact JSON. sig is the signature over the UTF-8 bytes of that payload string.

Every payload object contains at minimum:

json
{ "v": 1, "t": <server unix seconds>, "nonce": "<echo of request nonce>", "ok": <bool> }

A signed ok:false (wrong password, expired key, banned, and so on) is still a signed truth. Read error / code for messaging. An unsigned { "error": "...", "code": "..." } with a 4xx/5xx status is only for transport-level failures. Treat any unverifiable response as failure.

How to verify (every signed response)

  1. Read payload as an opaque UTF-8 string. Do not re-serialize it. The signature is over those exact bytes. (When you pull it out of the envelope JSON, only JSON-*unescape* the string value. Never round-trip it through a serializer.)
  2. base64-decode sig to get exactly 64 bytes (IEEE P1363 r || s).
  3. Verify ECDSA-P256-SHA256(sig, utf8(payload), embeddedPublicKey). If it is invalid, abort (do not parse the payload).
  4. Only then JSON-parse the payload.
  5. Check that payload.nonce equals the nonce you sent. If not, it is a replay or tamper, so abort.
  6. (Advisory) Check abs(localUnix - payload.t) <= 60. On drift, warn but do not hard-fail.

Verify with the OpenSSL CLI

The CLI verifies a DER signature, so convert the 64-byte P1363 r||s to DER first. You need the public key in pub.pem (SPKI), the exact payload bytes in payload.bin, and the raw 64-byte signature in sig.p1363.bin:

bash
# r = first 32 bytes, s = last 32 bytes.
# Build a DER ECDSA-Sig-Value: SEQUENCE { INTEGER r, INTEGER s }.
# (Use any P1363->DER helper; many languages expose this directly. Pseudo-DER below.)
#   der = SEQ( INT(r), INT(s) )   # mind the leading-zero rule for high-bit-set integers
openssl dgst -sha256 -verify pub.pem -signature sig.der payload.bin
# -> "Verified OK"  (or "Verification Failure")

If your public key is in base64 SPKI DER (the form the SDK embeds), wrap it to PEM first:

bash
{ echo "-----BEGIN PUBLIC KEY-----"; echo "<base64-spki>" | fold -w64; \
  echo "-----END PUBLIC KEY-----"; } > pub.pem

Verify (language-agnostic pseudo-code)

text
function handleResponse(httpStatus, body, sentNonce, pubKeySpkiDer):
    if httpStatus != 200:
        # unsigned transport error { error, code } - never trusted
        return FAIL(parseUnsignedError(body))

    env = jsonParse(body)
    if env.payload is not String or env.sig is not String:
        return FAIL("unsigned/unstructured response")

    payloadStr   = env.payload                 # opaque; DO NOT re-serialize
    payloadBytes = utf8(payloadStr)
    sig          = base64decode(env.sig)
    if length(sig) != 64: return FAIL("bad signature length")

    if not ecdsaP256Sha256Verify(sig_p1363 = sig,
                                  message  = payloadBytes,
                                  key      = importSpki(pubKeySpkiDer)):
        return FAIL("signature verification failed")     # HOSTILE - abort

    p = jsonParse(payloadStr)                  # only now
    if p.v != 1:            return FAIL("bad envelope version")
    if p.nonce != sentNonce: return FAIL("nonce mismatch - replay/tamper")
    if abs(localUnix() - p.t) > 60: warn("clock skew")   # advisory only
    return OK(p)                               # trust p.ok / p.code / fields

2. Nonce rules


3. Session lifecycle

  1. init returns an opaque session token plus app status. Required before anything else.
  2. login / register / license authenticates the session (server marks it active).
  3. check (heartbeat) runs every heartbeat seconds. Kick the user when the signed reply says the session, app, or key is no longer valid.
  4. logout (optional) ends the session.

The session token is opaque server-side state. Send it in the JSON body of every later call. Killing it server-side invalidates it on the next check.


4. Endpoints

All requests include app_id and nonce. All signed responses include v, t, nonce, ok. Fields marked ? are optional or conditional. expiry is unix seconds or null (null = lifetime).

POST /init

Establish a session and read app status / heartbeat interval.

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "version": "<client app version, optional>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
  "session": "<token>",
  "app_name": "AtlasApp",
  "app_status": "active",            // "active" | "maintenance" | "disabled"
  "status_message": "<text shown to user when not active>",
  "heartbeat": 10,                   // seconds between /check calls
  "hwid_required": true,
  "version_ok": true,                // false if app forces a version and it mismatches
  "latest_version": "1.4.0" }

If app_status != "active", show status_message and exit. If version_ok == false and the app forces a version, show an update notice and exit.

POST /register

Self-signup with a license (only if the app allows registration).

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>",
  "username": "<u>", "password": "<p>", "license": "<key>", "hwid": "<hwid>",
  "email": "<optional>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
  "error": "<optional>", "code": "<optional>",
  "expiry": null, "username": "<u>" }

Codes: ok · register_disabled · username_taken · invalid_license · license_used · license_banned · hwid_banned · ip_banned · bad_input.

POST /login

Username + password.

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>",
  "username": "<u>", "password": "<p>", "hwid": "<hwid>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
  "error": "<optional>", "code": "<optional>",
  "username": "<u>",
  "expiry": null,                    // null = lifetime
  "level": 0,
  "created_at": 0,
  "last_login": 0,
  "remaining_seconds": null }

Codes: ok · invalid_credentials · user_banned · hwid_mismatch · hwid_banned · ip_banned · license_expired · no_subscription · bad_input.

POST /license

License-key-only login (no username/password).

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>",
  "license": "<key>", "hwid": "<hwid>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
  "error": "<optional>", "code": "<optional>",
  "expiry": null, "level": 0, "remaining_seconds": null }

Codes: ok · invalid_license · license_expired · license_banned · hwid_mismatch · hwid_banned · ip_banned · bad_input.

POST /check - heartbeat

Call every heartbeat seconds.

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
  "valid": true,             // session still alive & authed & not killed
  "app_status": "active",    // re-checked every beat
  "status_message": "",
  "key_valid": true,         // bound key not expired / not banned
  "banned": false,           // user/hwid/ip banned since login
  "expiry": null,
  "remaining_seconds": null,
  "reason": "" }             // when ok/valid false: killed | expired | app_disabled | app_maintenance | banned

Kick the user immediately if ok == false OR valid == false OR app_status != "active" OR key_valid == false OR banned == true.

POST /var

Fetch an app variable.

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>", "name": "<var name>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
  "found": true, "value": "<string>", "code": "<optional>" }

The server enforces visibility. A variable marked auth-required returns ok:false, code:"auth_required" unless the session is authenticated. Public variables return on any valid session.

POST /log

Client-side logging to the app log and an optional Discord webhook. Best-effort, and it never blocks the client. session is optional.

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token, optional>",
  "level": "info",                   // "info" | "warn" | "error"
  "message": "<text>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true }

POST /logout

End the session.

Request

json
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>" }

Signed payload

json
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true }

5. Transport errors (unsigned)

Transport-level failures return an unsigned body with a 4xx/5xx status:

json
{ "error": "<human>", "code": "<machine>" }

HTTP statuses: 400 / 401 / 403 / 404 / 429 / 500. Common codes: unknown_app · bad_request · rate_limited (with a Retry-After header) · server_error. These are not signed. The client must treat any non-verifiable response as a failed call and must not trust its contents.


6. Versioning

The path is /api/v1. Breaking changes bump the path. payload.v is the envelope version (currently 1).


7. Public pingable endpoints (unsigned, informational)

Unlike every endpoint above, these are plain public GETs. No app_id/nonce in a body, no session, no signed envelope, no signature to verify. They carry only display and monitoring data and are safe to call from a website, launcher, uptime monitor, or Discord bot. Both send Cache-Control: public, max-age=15.

Informational only. Never gate execution on these. A fake or MITM server can return anything because there is nothing to verify. Security still rides on the signed /check heartbeat (§4); use that for enforcement. These endpoints just surface status and news to humans.

GET /api/v1/status/{app_id}

App status, status message, and a live online count.

Response

json
200 { "ok": true, "app_id": "...", "name": "AtlasApp",
      "status": "active",                 // "active" | "maintenance" | "disabled"
      "status_message": "...",
      "online": 0,                         // active sessions, last 5 min
      "time": 0 }                          // server unix seconds
404 { "ok": false, "error": "unknown_app" }

GET /api/v1/news/{app_id}

Published news/changelog items, ordered pinned-first then newest.

Response

json
200 { "ok": true, "app_id": "...",
      "news": [
        { "id": "...", "title": "...", "body": "...",
          "pinned": false, "created_at": 0, "updated_at": 0 }
      ],
      "latest": { "id": "..." },           // first item, or null when there is no news
      "time": 0 }                          // server unix seconds
404 { "ok": false, "error": "unknown_app" }

latest is the first element of news (a pinned item if any, otherwise the newest), or null when the list is empty.

GET /status/{app_id}

A human-readable public status and changelog HTML page (not JSON). It shows the same status and news as above, rendered for a browser. Link it from your site or Discord. It needs no SDK and no signing.