Skip to content

Accounts and Auth Internals

Kurt edited this page Jul 18, 2026 · 7 revisions

Accounts and Auth Internals

Audience: Engine Developer Status: ✅ Ready

telos-account owns identity: accounts, characters, trust tiers, and the OAuth flow that turns a browser sign-in into a signed token the rest of the fleet trusts. Auth is terminal-native and OAuth-only — there are no passwords and no SSH. The design keeps the account service off the hot path: it mints a short-lived signed session assertion that the world verifies offline, so gameplay never makes a per-action auth call.

Related: RPC & Protobuf (the Account service), Trust Tier Model and First-Admin Setup (the audience-facing views), Edge & Protocol (the gate side), Sysadmin OAuth Setup (deployment).

The broker-mediated device bridge

Login is a browser device bridge, not RFC 8628 device flow and not a typed link code — the "device code" is TelosMUD's own high-entropy handle. Only GitHub is implemented today (the migration comment lists Google/Discord, but no provider code exists for them).

sequenceDiagram
    participant C as Player (telnet)
    participant G as telos-gate
    participant A as telos-account (broker + gRPC)
    participant GH as GitHub OAuth
    C->>G: connect (account-backed gate)
    G->>A: StartDeviceAuth
    A-->>G: device_code + login URL
    G-->>C: prints the clickable URL, gate polls
    C->>A: opens the login URL in a browser
    A->>GH: redirect (CSRF state + PKCE verifier in a signed cookie)
    GH-->>A: callback with code
    A->>GH: exchange code with the PKCE verifier
    A->>A: resolve-or-create account by provider uid (never email)
    A-->>G: poll returns authed + account id + character list
    G->>C: character select / create
Loading

The device code has a 10-minute TTL; the flow cookie is HMAC-signed; the account is keyed on the immutable (provider, provider_uid) pair, never the user-settable email. Redis backs the short-lived device sessions — without it, the broker and device auth are disabled but the gRPC API still serves.

Signed session assertions

IssueSessionAssertion mints an Ed25519-signed envelope with claims {account, character, session, exp} (and the trust tier). The gate carries it in the Play stream's Attach frame; the world verifies it offline against the account's public key and captures the tier only from a signature-checked claim. This is why the world never calls the account service during play — the assertion is a self-contained, cryptographically bound capability. Signing/verify keys are supplied by the operator (TELOS_ACCOUNT_SIGNING_KEY on the account service, TELOS_ACCOUNT_VERIFY_KEY on every world shard); with none configured the assertion is empty, and a discoverable world refuses to boot unless TELOS_ALLOW_INSECURE is set. The gate authenticates to the account gRPC API with a shared caller token (TELOS_ACCOUNT_CALLER_TOKEN).

Accounts, characters, and chargen

  • An account comes into existence implicitly on first sign-in (default tier player); there is no separate provisioning step.
  • Character names are unique (CITEXT), reserved via ReserveName.
  • Chargen is content-driven (Model A): GetChargenFlow returns the content-defined step flow (point-buy, bundle choices) that the gate walks as prompts; CreateChargenCharacter validates the submission with the cost curve kept server-side. The chosen output is recorded and applied by the world on first spawn — the account service only reads content and validates. See Pack Entity Reference (chargen_defs).

Trust tiers (the account side)

The trust tier is stored on the account, signed into the assertion, and applied by the world as capability flags on next login. The account service is the authority:

  • Bootstrap: the TELOS_BOOTSTRAP_ADMIN config pin grants admin to the first account created for that GitHub login (not email), atomically with an audit row — first-run only. A telos-account set-tier break-glass CLI (host/DB access = authorization) is the recovery path. See First-Admin Setup.
  • Promote/demote: the in-game gate verbs call SetAccountTier, whose authorization is enforced here — the actor's tier is read from the authoritative store, promotion ceilings are enforced with a compare-and-set under a row lock (you can't grant above your own authority, and concurrent changes can't race), the path is fail-closed if the content trust ladder can't be validated, and every change writes an audit row to the shared append-only character_audit trail in the same transaction as the tier change (atomic — a promotion is never recorded-but-unapplied or vice versa). The effect lands on the target's next login. See Persistence & Durability → the audit trail.
  • The manage_tiers visibility bit: for visibility only, IssueSessionAssertion also resolves a manage_tiers capability bit — the same GrantsFlag(tier, admin) predicate SetAccountTier checks authoritatively — and returns it alongside the assertion (resolved even on a signing-less gate; fail-safe to false on an unknown account, a tier-read error, or an unavailable ladder). The gate caches it per login and uses it only to decide whether to intercept promote/demote: a non-staff actor's attempt falls through to a world Huh? before any parse, output, or SetAccountTier call, so the verbs neither leak their existence nor amplify into an account-service probe. This is defense-in-depth visibility only — a stale or spoofed too-high bit still hits the authoritative service refusal (fail-closed), and a too-low bit only costs a Huh? (fail-safe).

The ladder itself is a content-defined ordinal (default player 0 / builder 20 / admin 40, with gaps for pack-inserted rungs) that both the world and the account service load, so there's a single source of truth. The world-side application (reserved-flag denylist, rank comparison, the signed-handoff tier carry) is documented on Trust Tier Model.

The single-session lock

A character has at most one live session fleet-wide. The lock is heartbeated; a newer login anywhere in the fleet displaces the older connection (Edge & Protocol). This is what keeps a duplicated or hijacked stream from co-existing with the legitimate one.

Not implemented (schema exists, no code)

Do not document these as usable: passphrase login (an account_auth table exists but no Argon2/verify code — dropped in Phase 15), SSH pubkey login (an ssh_keys table exists but no SSH server; auth is OAuth-only), and the Google/Discord providers.

Clone this wiki locally