Skip to content

03 — Logging in

The client in chapter 02 connects with an arbitrary token to a local server — fine for a listen-server on 127.0.0.1. For a real, managed game you want a player identity: an access token minted by the lattice-auth service, which you later present to the director (to get a session token) and to the social service.

The important truth for a C/C++ integrator:

The core has no auth/login client — but it can make the call for you

lattice.h exposes no login, matchmaking, or social functions: there is no lattice_login(). What it does have is a mediated HTTP fetch that speaks https, so getting a token is a request you shape and the core performs, after which you hand the token bytes to lattice_runner_connect. Bringing your own HTTP client remains fully supported.
Verified against: absence of any login/auth/matchmake symbol in reference/include/lattice/lattice.h (only lattice_http_* fetch exists, below); docs-site/docs/getting-started/common-setup.md:35-52.

The control-plane HTTP surface (auth → token, director → session, social → friends) is a separate concern from the netcode. This chapter shows the auth call and the two ways a C app can make it. The auth endpoints are documented and verified per-line in the Unity tutorial chapter 03 — they are the same HTTP service regardless of client language, so those shapes apply to you unchanged.

The auth endpoints

lattice-auth exposes plain JSON HTTP. In local dev it runs on port 3005.

Endpoint Body Returns Auth
POST /guest { region?, device_fingerprint? } token response none
POST /login { email, password, device? } token response none
POST /register { email, password, region? } { account_id }no token none
POST /refresh { refresh_token } token response none

Verified against: port 3005 at run-all.sh:24 (PORT_AUTH=3005); the endpoint set + bodies are verified per-line in Unity chapter 03 against control-plane/lattice-auth/src/LatticeAuth/Program.cs and …/Api/Contracts.cs.

The token response (snake_case JSON):

POST /login (or /guest) → 200
{
  "access_token": "eyJ…",
  "refresh_token": "…",
  "expires_in": 3600,
  "account_id": "5f3c…"
}

Verified against: the TokenResponse(access_token, refresh_token, expires_in, account_id) contract documented in Unity chapter 03 (grounded there at Contracts.cs:17-21).

You keep the access_token and present it as a Bearer header to the director (chapter 04).

The core's mediated fetch speaks https and takes your headers, so a guest login is a config call plus a request. No HTTP client, no TLS library, no DNS code on your side.

Guest login through liblattice
/* 1. Egress is DENY-ALL until you say otherwise. Allow exactly the host:port you need. */
const char* allow[] = { "auth.example.com:443" };
lattice_http_egress_config eg = {0};
eg.allow = allow; eg.allow_count = 1; eg.rate_burst = 8; eg.rate_per_sec = 8.0;
lattice_http_configure(r, &eg);

/* 2. POST, with whatever headers the service wants. */
const char* body = "{\"region\":\"eu\"}";
lattice_http_header hdrs[1] = { { "Content-Type", "application/json" } };

uint64_t h = lattice_http_request_ex(r, LATTICE_HTTP_POST,
                                     "https://auth.example.com/guest",
                                     (const uint8_t*)body, (uint32_t)strlen(body),
                                     hdrs, 1);

/* 3. The answer arrives on a LATER tick — on_http_result, or poll like this. */
uint64_t got; int ok, status; uint32_t len; unsigned char buf[4096];
if (lattice_http_poll(r, &got, &ok, &status, buf, sizeof(buf), &len)) {
    /* status == 200: parse access_token out of buf with your JSON library */
}

Verified against: reference/include/lattice/lattice.h:887-893 (lattice_http_request_ex), reference/include/lattice/lattice.h:844-845 (lattice_http_configure), reference/include/lattice/lattice.h:907-909 (lattice_http_poll), reference/include/lattice/lattice.h:857-861 (lattice_http_header).

What the core does for you, and what it still does not

Certificates are verified against the system trust store and the hostname is checked. An https URL that cannot be verified fails — there is no flag anywhere in the public API to weaken that, because a flag that can be set in production by accident is worse than no flag. Redirects, chunked bodies and content encodings are handled by the platform's own HTTP stack (WinHTTP on Windows, libcurl on POSIX), not by code we wrote.

Ask once at startup whether the platform has a usable stack, so a missing runtime is a startup message rather than a mystery on your first login:

char err[256];
if (!lattice_http_tls_available(err, sizeof(err))) {
    /* err names what to install; https requests will be refused until it is there. */
}

Verified against: reference/include/lattice/lattice.h:898 (lattice_http_tls_available).

Three constraints remain yours to respect:

  • Egress is deny-by-default. Until lattice_http_configure names a "host:port", every request is rejected. An empty allow-list means deny-all — it is an SSRF and exfiltration defence, so the entry names the exact host and port, never the host on its own.
    Verified against: reference/include/lattice/lattice.h:823-840 (lattice_http_egress_config).
  • Main thread only. lattice_http_configure, lattice_http_request* and lattice_http_poll are called from the same thread that calls lattice_runner_tick, and results only ever surface there. If your login happens on a worker thread today, that thread must hand the request to the tick thread rather than calling in directly.
    Verified against: reference/include/lattice/lattice.h:795-797 (threading contract).
  • JSON is still yours. The core parses HTTP, not your payload. Pulling access_token out of the response needs nlohmann/json, RapidJSON or similar — that is what keeps liblattice language neutral and free of dependencies you did not choose.
Failure statuses are negative, so they never collide with an HTTP code
LATTICE_HTTP_NOT_ALLOWED  = -1,  /* host:port not on the egress allow-list  */
LATTICE_HTTP_RATE_LIMITED = -2,  /* rate limit exceeded                     */
LATTICE_HTTP_BAD_URL      = -3,  /* unparseable / unsupported scheme        */
LATTICE_HTTP_CONNECT_FAIL = -4,
LATTICE_HTTP_IO_FAIL      = -5,
LATTICE_HTTP_NO_TRANSPORT = -6,  /* no TLS stack on this platform           */
LATTICE_HTTP_BAD_HEADER   = -7   /* a header name/value could split the block */

Verified against: reference/include/lattice/lattice.h:802-810 (lattice_http_status).

Header values are checked, not sanitised

A CR, LF or NUL in a header value — or a colon in a name — would inject extra headers, so the request is refused with LATTICE_HTTP_BAD_HEADER rather than quietly rewritten. A caller that produced one has a bug, and hiding it would only move the discovery later.

Option B — bring your own HTTP client

Still perfectly reasonable, and the right answer in two cases: your server already links an HTTP stack you trust and would rather use one client for everything, or you need something the mediated fetch deliberately does not do (streaming, custom certificate pinning, a proxy policy of your own).

Use whatever you already have — libcurl, cpp-httplib, Boost.Beast — POST to auth, parse the token, and hand its bytes to connect:

Sketch: your own client, then connect
std::string accessToken = /* parsed from your own /guest response */;

std::string host = "1.2.3.4"; uint16_t port = 9000;
lattice_runner_connect(r, host.c_str(), port,
                       (const uint8_t*)accessToken.data(),
                       (uint32_t)accessToken.size());

Verified against: the lattice_runner_connect(runner, addr, port, token, token_len) signature at reference/include/lattice/lattice.h:374-375.

Embedders who need a different transport policy entirely can supply one: the fetch is built on an internal transport seam, so a custom implementation replaces the platform stack without changing anything above it.

Verified wiring status (honest)

  • Real and works: the netcode side — lattice_runner_connect(addr, port, token, token_len) accepts whatever token bytes you obtained. The mediated fetch is real, allow-listed, rate-limited, async, drained on the tick thread, and now speaks https with verified certificates, real DNS and caller-supplied headers.
    Verified against: reference/include/lattice/lattice.h:374-375, reference/include/lattice/lattice.h:887-893 (lattice_http_request_ex).
  • You must still write / bring: the JSON parse, and a hop to the tick thread if your login runs on a worker. The core has no auth client and no JSON parser — it makes the call, you read the answer.
    Verified against: reference/include/lattice/lattice.h:835-836; examples/README.md ("No external dependencies").
  • For learning / local play: skip auth entirely and use the arbitrary-token listen-server flow from chapter 02 — the path the tests verify end to end.
    Verified against: reference/tests/two_process.cpp:251-252.

Next: 04 — Lobbies & matchmaking, to turn that token into a game server endpoint.