Manual:GMCP Extensions
Automatic package installation
Mudlet supports automatic download and setup of Mudlet packages should the MUD server offer it. These packages could consist of graphical additions to your game or anything else you'd like your players to have loaded on their Mudlet profiles by default. This setup is automatic and carefree on part of the user, and handles package updates in a transparent manner as well.
setup
After GMCP is negotiated, the server should send a message via GMCP with:
Client.GUI {
"version": "<package version>",
"url": "<url>"
}
When Mudlet receives the message, it will do everything necessary to make the latest UI appear: if the user does not have a package, it will be downloaded and installed from the <url>, and if they do, but of an older version - it'll be upgraded. If they're already on the latest, nothing happens.
Ensure that the filename is the same as the package name, as Mudlet will use the filename in determining if the package is already installed.
For example:
Client.GUI {
"version": "39",
"url": "http://www.stickmud.com/mudwww/StickMUD.mpackage"
}
Note: Mudlet 4.18+ The JSON "version" parameter may also be an integer value
Note: there is an older, not preferred but still supported way is to send a message via GMCP with:
Client.GUI <package version>\n<url>
demonstration
Enable GMCP in settings, connect to Midnight Sun 2 (midnightsun2.org on port 3000) and create a character. A GUI will automatically install over GMCP once you finish character creation, and an overview of it is available here - a good idea to consider to make your MUD more visually appealing.
For an ATCP demo, connect to godwars2.org on port 3000. Mudlet will download the GW2 interface and set it up automatically.
Sample string sent by GodWars 2:
"Client.GUI 1\nhttp://www.godwars2.org/download/mudlet/GW2_GUI.mpackage"
updating
If you change the package version to any other text than what it previously was, Mudlet will consider it an update - it'll uninstall the old package and install the new package. This happens without prompting the user (since it can be kind of tricky if they decline), thus any modifications they would have done would be wiped - encourage users to make any modifications outside of the package and provide hooks for doing so.
For package versioning you can use any text you seem appropriate. Simple version numbers (start with 1, then 2, then 3) will do, as well as minor and/or patch versions like 1.2 and 1.2.3 or even 1.2.3-alpha (see https://semver.org/ for inspiration). You could also easily use the current date of release (like 2021-06-17 now - but make sure to indicate if you happen to release multiple versions on the same day), or even fancy special names for each release (how about http://sentimentalversioning.org/ for inspiration?)
Automatic map download
Mudlet can download a map for the user as soon they open the mapper for the first time, as well as re-download the map from the Mapper tab in settings:
The map can be either in the MMP standard (indicated by .xml at end of file name) or one exported from Mudlet.
Note: Recommend sending Client.Map before Client.GUI for best results.
setup
To let Mudlet know where it can download your map, send the following after GMCP has been enabled:
Client.Map {
"url": "https://..."
}
GMCP Extension for MUD Client Authentication
This document defines a GMCP extension that lets MUD clients automate sign-in around the game's own login screen: sending stored credentials, opening browser sign-in links, and reconnecting password-lessly with a server-minted token. Version 2 adds OAuth-based sign-in and the reconnect token to the password login defined by version 1.
Support in Mudlet
Mudlet has autofilled stored credentials over this extension - version 1 of it - since:
Version 2, which adds the browser sign-in, the saved reconnect token and the screenless reconnect that follows from it, arrived in:
Mudlet advertises Char.Login 2 in Core.Supports.Set. It keeps the reconnect token, the account and the provider in your operating system's keychain rather than in the profile, and Forget saved sign-in on the Connection tab of Settings clears them for that game.
Three details of Mudlet's version 2 client differ from this specification, and a server author should know about them:
- Mudlet does not read
secure_only. It refuses to replay a saved token on any connection that is not encrypted, whatever the field says. That is stricter than this document for a token minted over plain telnet, and never laxer. - In the client-driven appendix, Mudlet reads the nonce flag on
Char.Login.Defaultunder the namenonce, notnonce_required. A server that follows this document will not get an OpenID Connect nonce from Mudlet. - Mudlet accepts
successas a JSON boolean or as the exact string"true". The integer forms1and0, and mixed-case strings such as"True", are read as a failure rather than as the value they encode.
Rationale
Different MUDs have diverse login command formats, making it challenging for MUD clients to handle login consistently. This extension provides a standardized way to exchange authentication information through GMCP, simplifying client development and improving compatibility.
Two principles shape the design:
- The game owns the sign-in screen; the client owns automation. Every game must keep a textual, interactive login for the many clients that do not implement this extension, so that screen always exists and is always the most complete presentation of the game's account model - its providers, its character creation, its wording. This extension does not ask clients to replicate any of it. Instead it gives clients four machine-readable hooks around that screen: autofill stored credentials, open a sign-in URL the server pushes, save a reconnect token, and learn the outcome.
- A minimal client floor. A conformant client needs no OAuth machinery and no sign-in UI of its own. It opens URLs in a browser and stores an opaque string. A first-time sign-in happens once per player; reconnects happen thousands of times - the hooks optimize the common case.
Versioning
This extension is negotiated through the module version in Core.Supports.Set / Core.Supports.Add: a client advertises the highest major version it understands, for example Core.Supports.Set ["Char.Login 2", ...].
Because base GMCP negotiation is one-directional, the server reports the version it is speaking back to the client in a version field on Char.Login.Default - the effective negotiated version, min(client's advertised version, server's highest supported version). A client treats the absence of the field as version 1, and SHOULD echo the version it is acting on in a version field on each message it sends, so the server can reject a mismatch cleanly (with Char.Login.Result {"success": false, "message": "Unsupported Char.Login version"}) rather than misread a payload.
- Version 1 defined password-only login:
Char.Login.Default(withtypeand, for OAuth servers, alocation),Char.Login.Credentials, andChar.Login.Resultas the response to a credentials attempt. - Version 2 (this document) adds four things: the
versionreport/echo; the emptyChar.Login.Credentials {}defined as an explicit, immediate hand-off to the server's interactive sign-in; theChar.Login.URLpush that lets the server open the player's browser at the right moment; and the password-less reconnect pair,Char.Login.Token/Char.Login.Reconnect. It broadensChar.Login.Resultinto the terminal message of every flow, and relaxeslocationfrom required to optional (it now belongs to the optional client-driven appendix). A new major version is warranted because version 2 adds a client→server message and changes the scope of an existing one; a client must opt in by version before a server uses them.
Backward compatibility. A version 2 server remains interoperable with a version 1 client: it offers password-credentials, omits the version 2 fields and messages, and treats the exchange as version 1. In all cases a client may send Char.Login.Credentials {} to fall back to interactive login.
The field encoding rules below are not versioned. They describe how values of the declared types may appear on the wire, and apply equally to a version 1 exchange - the string form of success predates this document.
Design
The extension uses the Char.Login namespace with the following messages.
Field encoding
Several MUD drivers have no JSON boolean in their serializer at all - LDMud's json_serialize(), for example, maps integers to JSON numbers and offers no true/false - so a server obliged to report one has nothing correct to send. This is a property of the serializer, not of any particular field, so it applies wherever this document declares a field boolean.
A server MAY send any such field as a JSON boolean, as the string "true" / "false", or as the integer 1 / 0. Clients MUST accept all three forms. Servers SHOULD send a JSON boolean where the serializer can produce one, and SHOULD NOT vary the encoding of a given field from message to message within one connection.
String forms are matched case-insensitively. A value that is none of the forms above is not a boolean: a client MUST treat the field as absent and apply the default this document gives for it, rather than guess.
Absent is not false. Every optional boolean here documents what its absence means, and that default is not always false: secure_only inherits the transport it arrived on. A client MUST therefore distinguish "field absent" from "field present and false" instead of testing the payload for truthiness. For a token minted over TLS the two readings differ, and the truthy one silently widens the token's exposure - the exact outcome the field exists to prevent.
The same driver limits apply in the other direction, so a server SHOULD also accept the integer fields defined here (version) when they arrive as strings; client scripting layers stringify freely.
Server-side
Char.Login.Default
Sent in response to Core.Supports.Set, or as soon as telnet negotiation completes. It reports the negotiated version and the supported authentication methods.
A server sends it exactly once per connection. A client that advertises support across more than one message - Core.Supports.Set followed by Core.Supports.Add - MUST NOT be answered with a second Char.Login.Default: some clients read the two frames as one and reject the trailing bytes as garbage. The sole exception is the optional re-offer after a rejected attempt described under Char.Login.Reconnect. The obligation binds the server; a client MUST NOT rely on it, and treats an illegal repeat as at most a fresh offer to fold into work already pending - never as a license to repeat side effects (see Security and transport).
version(integer, optional; sent by version 2+ servers): the negotiatedChar.Loginversion for this session,min(client's advertised version, server's highest supported version). A positive, non-zero integer. Absent means version 1.type(array of strings, required): the supported method(s), ordered by the server's preference, most-preferred first:password-credentials: traditional username/password login.oauth: sign-in completes through the player's web browser (external identity providers, brokered by the server).
Char.Login.Default {"version": 2, "type": ["oauth", "password-credentials"]}
A version 1 exchange omits version:
Char.Login.Default {"type": ["password-credentials"]}
A server that is itself an OpenID Provider may include the additional fields defined in the client-driven appendix on an encrypted connection.
Note: the server does not enumerate its identity providers here, and the client does not choose one over GMCP. The provider choice happens where it always has - on the game's own sign-in screen (see below).
Char.Login.URL
Pushed by the server whenever the sign-in reaches a browser step - typically the moment the player picks a provider on the game's own login screen. A capable client opens the URL in the system browser so the player does not have to click or copy anything; the server SHOULD also print the URL in its own text (clickable where the client renders links, copy/paste otherwise), which remains the universal fallback for clients that ignore this message.
url(string, required): the fully-formed authorization URL to open in the player's default browser. The server constructs it (provider, scopes, redirect URI, PKCE, etc.); the client performs no OAuth machinery.nonce(string, optional): an opaque, single-use value tying this browser sign-in to the current session, so the server can correlate the provider callback with this connection. Not a usable credential on its own, so this message is safe over plain telnet.provider(string, optional): the id of the provider this URL signs in with (for examplediscord), letting the client label the handoff ("Opening your browser to sign in with Discord...").
Char.Login.URL {"url": "https://example.com/oauth/start?provider=discord&nonce=abc123", "nonce": "abc123", "provider": "discord"}
Consent and safety. A client MUST open only http/https URLs from this message, and only in the system browser - never hand other URI schemes to the operating system. Because the message can arrive unprompted, the client SHOULD confirm user intent before auto-opening; a practical test is whether the user has sent input on this connection since the last automatic hand-off (their provider choice on the game's screen is what triggered the push). The evidence is spent by the open: one player action authorizes at most one automatic hand-off, so a burst of pushed URLs buys one tab and the rest are offered as links. Absent fresh evidence, present the URL as a link to click or copy instead - a hostile or misbehaving server can then neither pop a browser at an idle player nor turn a single keystroke into a cascade of tabs.
Char.Login.Token
An opaque, server-minted reconnect credential the client stores in its profile for password-less reconnects. The server sends it at its discretion once the session is authenticated, and MAY send it again at any time on that connection, any number of times - typically to rotate the token on each successful reconnect. The most recent token supersedes every earlier one: the client overwrites its stored copy and treats the previous token as dead. No ordering relative to Char.Login.Result is implied - a server may mint the token before the terminal result, after it, or both. Whether to ask the player first ("stay signed in on this device?") is the server's decision, gathered in its own flow; the client's job is mechanical - save what arrives, overwrite on rotation, and offer a local way to forget it.
account(string, required): the character or account the token authenticates, in the same form as theaccountfield ofChar.Login.Credentials(for examplemyaccount:mycharacter). A server with multi-character accounts SHOULD mint the character-qualified form: the client replays it verbatim, so a laterChar.Login.Reconnectcan land directly in the character this device last played - a truly screenless reconnect - with the character roster as the fallback whenever the named character no longer belongs to the account.token(string, required): an opaque, server-owned bearer secret. The client stores it verbatim and never interprets it.secure_only(boolean, optional): whether this token may be replayed only over an encrypted transport (telnets:/// TLS). When absent, the token inherits the transport it arrived on: a token issued over an encrypted connection is encrypted-only, and a token issued in the clear may be replayed on either. A server that issues tokens over TLS but is content to accept them on plain telnet says so explicitly withfalse- silence never widens the token's exposure beyond what the issuing connection already risked. Because absence andfalsecarry different meanings here, a client MUST read this field as described in Field encoding and never by testing the payload for truthiness.
Char.Login.Token {"account": "myaccount:mycharacter", "token": "opaque-reconnect-token", "secure_only": true}
The identity provider's own access and refresh tokens never reach the client or the wire - only this opaque, server-minted reconnect token does.
Replay transport. The token is a bearer secret: replayed in cleartext it hands the account to anyone on the path, without the player's password ever being involved. A client MUST NOT send Char.Login.Reconnect carrying a token whose transport requirement the current connection does not meet. It stores the requirement alongside the token, skips the token when the transport disqualifies it, and continues down its ladder - the resume form or the interactive hand-off - exactly as if no token were saved; to the server this is indistinguishable from a client with nothing stored, so no protocol accommodation is needed. The client SHOULD tell its player why the saved sign-in was not used, and MUST NOT discard the token, which remains valid for the next connection on an adequate transport.
Char.Login.Result
The terminal message of every authentication flow - password, browser sign-in, and reconnect. The server sends it once the outcome is known.
success(boolean, required): whether authentication succeeded. May arrive in any of the forms described in Field encoding - a JSON boolean, the string"true"/"false", or the integer1/0- all of which clients MUST accept. The string form is not merely legacy: several MUD drivers have no JSON boolean in their serializer at all.message(string, required whensuccessis false): a human-readable explanation, such as "Invalid credentials" or "Reconnect token expired".
Char.Login.Result {"success": true}
Char.Login.Result {"success": false, "message": "Invalid credentials"}
A rejected sign-in or reconnect token is reported this way and never falls through to a password or password-creation prompt.
Client-side
Each client→server message MAY carry a version field echoing the version received in Char.Login.Default; a version 2 client SHOULD include it.
Char.Login.Credentials
After receiving Char.Login.Default with type including password-credentials, a client holding stored credentials sends them. A complete stored pair is the player's explicit choice of sign-in method: it is the client's default, taking precedence over a saved reconnect token (see Guidance for clients). A partial pair - a password without a name, or vice versa - is never sent and never blocks the other methods.
account(string, required): character name or player account name. For games that implement both, the character name follows a colon (:), for examplemyaccount:mycharacter.password(string, required): the password. Servers are encouraged to implement TLS over telnet so passwords transit securely.version(integer, optional): the negotiated version the client is acting on.
Resume form. A client that previously completed a browser sign-in - and therefore learned its provider from the provider field of Char.Login.URL - may instead send the account with that remembered provider and no password:
Char.Login.Credentials {"account": "myaccount:mycharacter", "provider": "discord", "version": 2}
This asks the server to restart the browser sign-in for that provider directly, replying with Char.Login.URL, so a player whose reconnect token has expired or been revoked is never sent back to a provider menu to answer "which provider did I use here?". The client remembers the provider on the player's behalf, in the same protected store as the reconnect token. To the server it discloses nothing the server did not itself send on this connection's predecessors; the server treats the value only as the flow to start (account-provider binding is verified after the browser sign-in completes, exactly as in the interactive flow, so the resume form confirms nothing about which provider an account uses). To anyone on the path of a cleartext connection it discloses the account name and its identity provider - identifiers, not credentials, comparable to what the game's own interactive login reveals on the same wire; this design treats the pairing as non-secret, and a player or game that considers it sensitive should be on telnets://, as for everything else it values.
A worked example is given in the resume flow.
A client with nothing to send - or one that simply prefers the game's own screen - hands off with an empty object:
Char.Login.Credentials {}
This is the explicit interactive hand-off: "the user will sign in on your screen - proceed with your interactive login now." It may be sent in response to any advertised method. A server that receives it SHOULD begin its interactive sign-in immediately rather than wait longer; a client that holds no stored token, provider, or credentials SHOULD send it promptly after Char.Login.Default.
Silence is not a hand-off. A client that advertised Char.Login but has sent nothing yet may be gathering credentials - typically prompting the player for a password it deliberately does not store, a version 1 behaviour that remains fully valid. The server SHOULD wait a generous window (long enough for a player to type) before falling back to its interactive screen, and waiting silently is correct: the player is looking at their client's prompt, not the game's. Only the explicit {} - or a completed attempt - ends the wait early.
Char.Login.Reconnect
On a later connection, a client that previously stored a Char.Login.Token replays it to log in without a browser or password. The replay is subject to the token's transport requirement (see Char.Login.Token): a token the current connection may not carry is skipped, not sent:
account(string, required): the saved account.token(string, required): the saved opaque token.version(integer, optional): the negotiated version the client is acting on.
Char.Login.Reconnect {"account": "myaccount:mycharacter", "token": "opaque-reconnect-token"}
The server replies with Char.Login.Result. On success it may rotate the token by sending a fresh Char.Login.Token.
After a rejection. A server SHOULD close the connection once it has sent Char.Login.Result {"success": false} for a reconnect attempt. The client discards the dead token, keeps the account and provider, and reconnects - the fresh connection brings a fresh Char.Login.Default, which is where the next attempt begins. A client MUST NOT replay a token that was rejected on this connection. (Replaying a different token is permitted - see the shared-store rotation case in Guidance for clients.) A server MAY instead hold the connection open and re-send Char.Login.Default to reopen negotiation; this is the one case where a second Char.Login.Default is legal, and a client MUST treat it as a new attempt rather than a protocol error. A rejected token never falls through to a password path.
Messages at a glance
| Direction | Message | Payload | Purpose |
|---|---|---|---|
| Server → Client | Char.Login.Default |
{"version"?: <int>, "type": [...]} |
report negotiated version and supported methods |
| Client → Server | Char.Login.Credentials |
{"account": "<id>", "password": "<secret>", "version"?: <int>}, {"account": "<id>", "provider": "<id>", "version"?: <int>}, or {} |
autofill stored credentials, resume the remembered provider's browser sign-in, or hand off to the game's screen |
| Server → Client | Char.Login.URL |
{"url": "<authorize-url>", "nonce"?: "<id>", "provider"?: "<id>"} |
open this in the system browser |
| Server → Client | Char.Login.Token |
{"account": "<id>", "token": "<opaque>", "secure_only"?: <bool>} |
a reconnect credential to save in the profile |
| Client → Server | Char.Login.Reconnect |
{"account": "<id>", "token": "<opaque>", "version"?: <int>} |
replay the saved token to log in with no screen at all |
| Server → Client | Char.Login.Result |
{"success": <bool>, "message"?: "<text>"} |
terminal status of any flow |
A <bool> above is any of the forms allowed by Field encoding - a JSON boolean, "true"/"false", or 1/0 - and an optional one carries its own documented meaning when absent, which is not always false.
(The optional client-driven appendix adds fields to Char.Login.Default and one client→server message, Char.Login.AuthCode.)
Who owns the sign-in screen
The server presents all sign-in choices - which providers exist, in what order, with what wording - on its own interactive screen, exactly as it does for clients without this extension. Plain numbered menus work everywhere; games may enhance them with clickable links (for example OSC 8 hyperlinks) where the connecting client supports them. Those presentation niceties are negotiated out of band and are orthogonal to this extension.
The client contributes what only it can do, silently: replay a saved token, autofill stored credentials, resume a remembered provider, open a pushed URL, save a fresh token, and act on the result. It renders no sign-in UI of its own, which also means the two sides never race to present competing screens: a version 2 client either sends a message promptly or hands off promptly. The server still allows for the client that does neither - it may be prompting the player for an unsaved password, version 1 style - by waiting a generous window before its interactive screen takes over (see Char.Login.Credentials).
The resulting first-connection experience on a capable client: the game's login screen appears immediately; the player picks a provider there; the browser opens by itself; they authorize; the server logs them in and mints a token. Every connection after that is instant and screenless via Char.Login.Reconnect.
Flows
Password credentials
- Client connects and sends:
Core.Supports.Set ["Char.Login 2", ...] - Server responds with:
Char.Login.Default {"version": 2, "type": ["password-credentials"]} - Client sends:
Char.Login.Credentials {"account": "username", "password": "password", "version": 2} - Server validates and replies with:
Char.Login.Result {"success": true}
A version 1 client uses this same flow without the version fields.
Browser sign-in (server-owned screen)
- Client connects and sends:
Core.Supports.Set ["Char.Login 2", ...] - Server responds with:
Char.Login.Default {"version": 2, "type": ["oauth", "password-credentials"]} - Holding no saved token or credentials, the client hands off immediately:
Char.Login.Credentials {} - The server begins its normal interactive login at once, printing its own sign-in menu.
- The player picks a provider on the game's screen - types a number or clicks a link.
- The server pushes
Char.Login.URL {"url": "https://example.com/oauth/start?provider=discord&nonce=abc123", "nonce": "abc123", "provider": "discord"}and also prints the URL in its own text. - The player has sent input this connection (their menu choice), so the client auto-opens the URL; the player signs in and consents at the provider.
- The provider redirects back to the server, which exchanges the authorization code server-side and authenticates the session. Nothing secret crossed the GMCP connection.
- The server mints a reconnect credential:
Char.Login.Token {"account": "myaccount:mycharacter", "token": "opaque-reconnect-token"}; the client saves it. - Server confirms with:
Char.Login.Result {"success": true}
Later reconnect (password-less, screenless)
- Client connects and sends:
Core.Supports.Set ["Char.Login 2", ...] - Server responds with:
Char.Login.Default {"version": 2, "type": ["oauth", "password-credentials"]} - Having a saved token (and no stored credentials, which take precedence), the client sends:
Char.Login.Reconnect {"account": "myaccount:mycharacter", "token": "opaque-reconnect-token", "version": 2} - On success: the server connects the player - directly into the character the token names, when the account still owns it - may rotate the token with a fresh
Char.Login.Token(the client overwrites its copy), then confirms withChar.Login.Result {"success": true}. - On failure: the server sends
Char.Login.Result {"success": false, "message": "Reconnect token expired"}and closes the connection; the client drops only the dead token, keeps the account and provider, and reconnects, continuing with the resume flow below (or the interactive hand-off when no provider is remembered).
When the reconnect token has expired or been revoked but the client still remembers the account and provider from an earlier sign-in, one browser visit - and nothing else - re-establishes the session:
- Client connects and sends:
Core.Supports.Set ["Char.Login 2", ...] - Server responds with:
Char.Login.Default {"version": 2, "type": ["oauth", "password-credentials"]} - Holding no usable token but remembering the account and provider, the client sends the resume form:
Char.Login.Credentials {"account": "myaccount:mycharacter", "provider": "discord", "version": 2} - The server starts that provider's browser flow directly and pushes
Char.Login.URL {"url": "...", "nonce": "...", "provider": "discord"}- no provider menu appears. - The player authorizes in the browser (typically one click - the provider session usually still exists); the provider redirects back to the server, which validates the account-provider binding.
- The server connects the player - directly into the character named by the account hint, when the account still owns it - mints a fresh per-device
Char.Login.Token, and confirms withChar.Login.Result {"success": true}.
Token lifecycle
- Rotation (single-use): on each successful reconnect the server may issue a fresh token and invalidate the one just used, shrinking the replay window of any captured token to near zero.
- One token per device, several per account: a player signs in from more than one machine (and more than one client). A server SHOULD hold a small set of concurrent tokens per account - each minted for one client install, rotated in place, with its own lifetime - rather than a single slot that every new device silently invalidates, which otherwise forces a browser re-pairing on every device switch. The token itself is the device's identity: opaque, anonymous, and individually revocable. Servers SHOULD NOT fingerprint devices to tell them apart - the token already does that without collecting anything about the player's hardware.
- Revocation: the server may revoke one token or all of them at any time - on password change, provider unlink, or an explicit sign-out-everywhere. The next reconnect fails cleanly and the client falls back to the resume form or the hand-off.
- No password fallback: a rejected token is always reported via
Char.Login.Result {"success": false}, never routed into a password or password-creation prompt.
Security and transport
The server is untrusted. A MUD client connects to arbitrary servers of the player's choosing, so every server→client message in this extension must be safe to receive from a hostile peer. Nothing a server sends may, by itself, spend the player's secrets on a weaker transport than they were earned on, launch the player's browser without their intent, or consume the client machine's resources in proportion to how much the server transmits. The rules in this section, the consent rule on Char.Login.URL, and the transport rules on Char.Login.Reconnect and Char.Login.AuthCode are all instances of that principle; a client SHOULD apply it to any situation this document does not enumerate.
- The reconnect token is an opaque, server-owned bearer secret carried in its own message, never conflated with the game password. Over plain
telnet://it travels in cleartext like a password - a long-standing MUD reality; single-use rotation, a sensible TTL, and a login-only scope limit the risk. A server may choose to issue and accept tokens only overtelnets://, and the default replay rule enforces the half of that choice the server cannot see: a token issued over an encrypted connection is never replayed in the clear unless the server explicitly allowed it withsecure_onlyfalse (see Char.Login.Token). - A malformed or unrecognised value in a security-bearing field is not a licence to pick the permissive reading. A client that cannot decode
secure_onlyMUST fall back to that field's documented default - inherit the issuing transport - and never to "no requirement"; see Field encoding. - In the browser sign-in, only the URL and an opaque nonce cross the GMCP connection; the authorization code is exchanged out of band over HTTPS. The flow is therefore safe over plain telnet.
- Clients MUST validate that any
Char.Login.URLishttp/httpsbefore opening it, and SHOULD gate auto-opening on evidence of user intent (input sent this connection), consuming that evidence with each automatic open so one action never authorizes more than one hand-off. - Clients SHOULD treat credentials, tokens, and any buffers holding them as secrets: clear them from memory once sent or stored.
- A client's costly side effects - credential-store reads, browser hand-offs, network requests - MUST be bounded by the player's actions and by wall clock, never by the number of frames the server sends. The obligations this document places on servers (for example,
Char.Login.Defaultexactly once per connection) do not defend the client, because a hostile server ignores them: a client folds a burst of identical or repeated messages into one pending response (say, at most one sign-in attempt per second) and drops deferred work whose connection has since closed, rather than trusting the server's discipline.
Guidance for servers
- Own the interactive screen, and wait well. Keep your textual sign-in first-class - most connecting clients have nothing else, and it is the natural home for provider choice, character creation, and "remember me" consent. Treat
Char.Login.Credentials {}as "proceed interactively now" and begin your normal login immediately - presenting the interactive welcome exactly once however the hand-off arrives. But treat silence differently: a client that advertisedChar.Loginand has said nothing may be prompting its player for an unsaved password (valid since version 1), so give it a generous window - silently, since the player is at their client's prompt - before the interactive screen takes over. A GMCP client that never advertisedChar.Loginneeds no wait at all. - A missing reconnect is not a missing client. A client holding a token it may not replay on this transport behaves exactly like one holding nothing: it sends the resume form or the interactive hand-off. Do not infer from the absence of
Char.Login.Reconnectthat the device lost its token - mint a fresh one after the sign-in completes, as usual, on whatever transport the new token should be scoped to. - Dispatch client requests in your advertised order. When more than one
Char.Loginrequest could apply, process them in the same preference order you advertised intype; a reconnect token is not one of the advertised methods and is verified ahead of them. Let incomplete credentials (a missing account or password) fall through to the next method rather than dead-ending the sign-in, scrubbing any partial secret either way. - Push the URL when the browser step arrives, with the
providerlabel, and still print it in your own text so every other client can click or copy it. - Mint and manage tokens deliberately. Issue
Char.Login.Tokenafter a successful sign-in regardless of which screen drove it; prefer single-use rotation; set a sensible TTL; revoke on password change or provider unlink. Hold a small set of tokens per account - one per client install, rotated in place - so a player's other devices stay signed in, and never fingerprint devices to tell them apart: the token already is the device's identity. Mint theaccountin its character-qualified form (myaccount:mycharacter) so each device's reconnect goes straight into the character it last played, skipping the roster. Gather any "remember me" consent in your own flow - clients treat token receipt as your instruction to save. Decide the token's replay transport when you mint it: absentsecure_only, a token issued over TLS is encrypted-only, so a game that accepts reconnects on plain telnet must sendsecure_onlyfalse deliberately rather than get it by accident. If you enforce a transport requirement, refuse a disqualified token without consuming it - it is still valid on the transport it was issued for. Close the connection after rejecting a reconnect token rather than falling back to another method on it. - Encode fields for the client, not for your driver. Send booleans as JSON booleans where your serializer can produce them, and otherwise pick one of the forms in Field encoding and keep to it for the whole connection. Accept a client's echoed
versionwhether it arrives as an integer or a string. - Report the negotiated version in
Char.Login.Defaultand gate version 2 messages on the client having negotiated version 2. - Send
Char.Login.Resultas the terminal message of every flow. If a client echoes a version you cannot honour, reject with{"success": false}rather than misparse the payload.
Guidance for clients
- A minimal client is fully conformant with: handle
Char.Login.Result; openChar.Login.URL(with the safety checks above); sendChar.Login.Credentials {}when you have nothing stored. SavingChar.Login.Tokenand replaying it withChar.Login.Reconnectis the single highest-value addition - it makes every later connect instant. - Decide promptly and deterministically on
Char.Login.Default, in this order: (1) when the game offerspassword-credentialsand the profile stores a complete character name and password, autofillChar.Login.Credentials- storing them is the player's explicit choice of sign-in method, so it is the default and no separate setting is needed; (2) otherwise, replay a saved reconnect token the current connection's transport is allowed to carry (see Char.Login.Token); (3) otherwise, with an account and provider remembered from an earlier browser sign-in, send the resume form ofChar.Login.Credentials; (4) otherwise, hand off withChar.Login.Credentials {}and let the game's screen drive. Never let a partial credential (a password without a name, or vice versa) block the later rungs. Stored credentials outrank a saved token because they name the exact character the player wants to play, whereas the token names whatever account last signed in; the token serves the password-less profile. - Decode declared booleans in every allowed form, per Field encoding, and keep "absent" distinct from "false" in your parser rather than collapsing the payload to a truthy test.
secure_onlyis the field that punishes the shortcut: missing means "inherit the issuing transport", which for a TLS-minted token is the strict answer, not the lax one. Treat any value you cannot decode as absent. - Persist reconnect tokens with their provider and transport: on
Char.Login.Token, save the account, the token, the provider learned fromChar.Login.URL, and the token's transport requirement (secure_only, defaulted from whether the issuing connection was encrypted), together, in protected storage (a keychain or equivalent - never a plaintext settings file), and overwrite on rotation. Accept it whenever it arrives - before or afterChar.Login.Result, and more than once on the same connection. On a rejectedChar.Login.Reconnect, first re-read the store: if the saved token no longer matches the one just sent, another running instance of the client sharing the store rotated it - replay the fresh token instead of discarding. If it does match, the token is truly dead: discard the token but keep the account and provider, then reconnect - the next attempt begins at the fresh connection'sChar.Login.Defaultand can use the resume form instead of sending the player back to a provider menu. Latch the rejection before any asynchronous store read, so that whicheverChar.Login.Defaultcomes next - on the new connection, or re-offered by a server that held this one open - does not replay the token that was just rejected. A local "forget this sign-in" control clears all of it. - Echo the version you are acting on; treat an absent server version as version 1.
Appendix: client-driven OAuth (advanced, optional)
A server that is itself an OpenID Provider may additionally let an OIDC-capable client run the authorization request end to end - useful when the client can offer a smoother native handoff than the brokered flow. This appendix is entirely optional in both directions: servers that broker external providers omit it, and clients that do not implement it ignore its fields and use the flows above.
Additional Char.Login.Default fields (sent only over an encrypted transport, telnets:// / TLS):
location(string): the server's OpenID Connect Discovery document (.well-known/openid-configuration).client_id(string): a public OAuth client identifier; PKCE protects it, no secret involved.scopes(array of strings, optional): scopes to request; defaults to["openid"].nonce_required(boolean, optional): when true, the client SHOULD include an OpenID Connectnoncein the authorization request. The name is deliberately distinct from the stringnoncecarried byChar.Login.URLandChar.Login.AuthCode: no key in this namespace should hold two types, or a client decoding by field name would read an opaque nonce string as a boolean true.
Char.Login.AuthCode (client → server): after capturing the authorization code at its loopback redirect, the client submits it; the server performs the token exchange and validation, so provider tokens never reach the client.
code(string, required): the captured authorization code.code_verifier(string, required): the PKCE verifier matching thecode_challengesent in the authorization request.redirect_uri(string, required): the redirect URI used in the authorization request, replayed verbatim in the token exchange.nonce(string, required when the server advertisednonce_requiredtrue): the OpenID Connectnoncethe client placed in the authorization request. The server MUST verify the ID token'snonceclaim against this value during the token exchange and reject the sign-in on a mismatch; without this field thenonce_requiredadvertisement would ask the client to generate a value that no verifying party ever learns.version(integer, optional): the negotiated version.
Flow: the client fetches location to find the authorization_endpoint; generates a PKCE verifier with an S256 challenge (REQUIRED) and a random state; starts a loopback listener (RFC 8252; RECOMMENDED) as the redirect URI; opens the browser to the authorization endpoint; verifies state on the redirect and captures code; sends Char.Login.AuthCode. The server exchanges the code at its token_endpoint, validates the ID token - including its nonce claim, when one was requested - authenticates the session, may issue Char.Login.Token, and confirms with Char.Login.Result.
Consent: This flow starts from fields on Char.Login.Default, which arrives before the player has sent anything, so the input test of Consent and safety cannot yet be met. Connecting to a game whose sign-in offer includes these fields is itself the request to sign in - once: a client MAY auto-open the flow's first authorization URL on a connection without prior input, and this once-per-connection allowance is the only unprompted hand-off the connection gets, however many frames the server sends. Every later authorization URL on the same connection - from this flow or from Char.Login.URL - needs fresh input, exactly as in the server-driven flow. A client that cannot open the URL, or declines to, presents it as a link to click or copy; the loopback listener stays up either way, so the sign-in still completes by hand.
Transport requirement: Char.Login.AuthCode carries the authorization code and PKCE verifier together, which on a cleartext connection would let an eavesdropper redeem the code at the provider. A client MUST NOT send it - and a server MUST NOT accept it - over an unencrypted connection, and a server MUST advertise location/client_id only over TLS so no client ever begins the flow in the clear.
Fit: only genuinely public OIDC providers qualify (a self-hosted OP, or the game itself). External providers with no discovery document, confidential secrets, or non-OAuth identity systems are reachable only through the server's own brokered screen, which the base flows above already cover.
Reference implementation
A working server-side implementation exists in StickMUD: the interactive hand-off, the pushed Char.Login.URL against real providers (Discord, GitHub, Google, Microsoft), Char.Login.Token issuance from the game's own screen with secure_only scoped to the issuing transport and enforced on replay, Char.Login.Reconnect with rotation, and revocation on password change, provider unlink, and sign-out-everywhere. Its driver has no JSON boolean, so it sends booleans in string form - the case the encoding rules above exist for. Client-side, Mudlet implements version 2, including the reconnect flow, the resume form and the client-driven appendix, with the three deviations listed under Support in Mudlet.
Implementation considerations
- Implement this extension according to the GMCP protocol specifications.
- Messages must be well-formed JSON objects.
- Encode field values as Field encoding describes, and parse incoming values by that rule rather than by your language's truthiness.
- Servers must validate incoming authentication information securely.
- Clients should handle errors and unexpected server responses gracefully.
Conclusion
This GMCP extension keeps the game in charge of its own sign-in screen while giving clients the hooks to automate around it - culminating in password-less, screenless reconnects. The result is a floor low enough for every client and a ceiling high enough for OAuth-based sign-in, without burdening either side with the other's presentation.
Note: This document describes version 2 of the Char.Login extension. Version 1 (password-credentials only) remains valid and is what a server offers to clients that negotiate Char.Login 1. This is a draft proposal, and further community input and refinement may be necessary before finalizing the specification.
