# Authentication System - MinecraftConsoles ## Overview 3-packet handshake between client and server, after PreLogin and before LoginPacket. Three schemes: **Mojang** (online verification via sessionserver.mojang.com), **ely.by** (online verification via authserver.ely.by), and **offline** (server-generated UUID from username). Replaces the old system that relied solely on console platform XUIDs (Xbox Live, PSN) and had no identity verification for the Windows x64 build. --- ## Packet Architecture | Packet | ID | Direction | Description | |---|---|---|---| | `AuthSchemePacket` | 170 | Server -> Client | Server offers auth schemes + challenge | | `AuthResponsePacket` | 171 | Client -> Server | Client picks a scheme and sends credentials | | `AuthResultPacket` | 172 | Server -> Client | Server confirms/rejects + assigns UUID and skin | The `LoginPacket` serves as the handshake acknowledgement — no dedicated ACK packet needed. ### Handshake Flow ``` CLIENT SERVER | | | ---- PreLoginPacket (netcode version) ----> | | <--- PreLoginPacket (UGC, save info) ------ | | | | <--- AuthSchemePacket (170) --------------- | schemes=["mojang"|"elyby"|"offline"], serverId="a1b2c3..." | | | ---- AuthResponsePacket (171) ------------> | scheme="mojang", uuid, username | | | [Server verifies with Mojang on a | | background thread + downloads skin] | | | | <--- AuthResultPacket (172) --------------- | success=true, assignedUuid, assignedUsername, skinData | | | ---- LoginPacket (1) ---------------------> | Completes auth handshake | | | <--- LoginPacket (1) --------------------- | Server responds with world data | | ``` --- ## Packet Details ### AuthSchemePacket (ID 170) **Direction:** Server -> Client Sent right after PreLogin response. Contains supported auth schemes and a random challenge. ```cpp class AuthSchemePacket : public Packet { vector schemes; // "mojang", "elyby", "offline" wstring serverId; // random hex challenge (20 chars) }; ``` **Wire format:** | Field | Type | Notes | |---|---|---| | `count` | int32 | Number of schemes (max 16) | | `schemes[i]` | UTF string | Scheme name (max 32 chars) | | `serverId` | UTF string | Hex challenge (max 64 chars) | **Read validation:** - Scheme count capped at 16 - Only `"mojang"`, `"elyby"`, and `"offline"` accepted; unknown strings silently discarded **Server behavior:** - `onlineMode=true`: offers only `["mojang"]` - `onlineMode=true, authProvider="mojang"`: offers `["mojang"]` - `onlineMode=true, authProvider="elyby"`: offers `["elyby"]` - `onlineMode=false`: offers `["mojang", "offline"]` --- ### AuthResponsePacket (ID 171) **Direction:** Client -> Server Client picks a scheme and sends credentials. ```cpp class AuthResponsePacket : public Packet { wstring chosenScheme; // "mojang", "elyby", or "offline" wstring mojangUuid; // dashed 128-bit UUID wstring username; // player username (max 16 chars) }; ``` **Wire format:** | Field | Type | Notes | |---|---|---| | `chosenScheme` | UTF string | max 32 chars, validated against known values | | `mojangUuid` | UTF string | max 64 chars | | `username` | UTF string | max 16 chars | **Read validation:** - Scheme reset to `""` if not `"mojang"`, `"elyby"`, or `"offline"` - Username validated char by char: only `[a-zA-Z0-9_]`. Invalid char clears the entire username **Client side ("mojang" scheme):** 1. Calls `MCAuth::JoinServer(token, uuid, serverId)` to `sessionserver.mojang.com/session/minecraft/join` 2. On success, sends AuthResponsePacket with scheme `"mojang"` 3. On failure, disconnects with `eDisconnect_AuthFailed` **Client side ("elyby" scheme):** 1. Calls `MCAuth::ElybyJoinServer(token, uuid, serverId)` to `authserver.ely.by/session/minecraft/join` 2. On success, sends AuthResponsePacket with scheme `"elyby"` 3. On failure, disconnects with `eDisconnect_AuthFailed` **Client side ("offline" scheme):** 1. Sends AuthResponsePacket directly with UUID and username from the local session --- ### AuthResultPacket (ID 172) **Direction:** Server -> Client Server communicates the verification result and assigns the player's final identity. ```cpp class AuthResultPacket : public Packet { bool success; wstring assignedUuid; // final UUID assigned by the server wstring assignedUsername; // final username wstring errorMessage; // empty if success=true wstring skinKey; // memory-texture key (e.g. "mojang_skin_{uuid}.png") vector skinData; // raw PNG skin bytes }; ``` **Wire format:** | Field | Type | Notes | |---|---|---| | `success` | bool | 1 byte | | `assignedUuid` | UTF string | max 64 chars | | `assignedUsername` | UTF string | max 64 chars | | `errorMessage` | UTF string | max 256 chars | | `skinKey` | UTF string | max 256 chars | | `skinSize` | int32 | skin data length | | `skinData` | byte[] | raw PNG (max 32KB) | **Skin validation:** - Max 32KB (a 64x64 RGBA skin PNG is ~4KB compressed) - Excess bytes consumed to keep stream synchronized but not stored - Client validates with `MCAuth::ValidateSkinPng()` (PNG magic, 64x32 or 64x64 dimensions) --- ## Server-Side Verification Flow ### "mojang" Scheme (Online Mode) ``` PendingConnection::handleAuthResponse() | +--> m_authState = eAuth_Verifying +--> Spawns detached std::thread: | +--> MCAuth::HasJoined(username, serverId) | [GET sessionserver.mojang.com/session/minecraft/hasJoined] | +--> On success: MCAuth::FetchSkinPng(skinUrl) | [Downloads skin PNG from Mojang CDN] | +--> Writes result to AuthVerifyResult (mutex-protected) +--> ready.store(true) PendingConnection::tick() [main thread] | +--> Checks m_authVerifyResult->ready +--> On success: | +--> Sends AuthResultPacket(success=true, uuid, username, skinKey, skinData) | +--> m_authState = eAuth_WaitingAck | +--> On failure: +--> Sends AuthResultPacket(success=false, errorMessage) +--> Disconnects client ``` ### "offline" Scheme ``` PendingConnection::handleAuthResponse() | +--> Checks that server->onlineMode == false +--> Generates offline UUID: GameUUID::generateOffline(username) | [UUID v3, namespace "OfflinePlayer:" + username] +--> Sends AuthResultPacket(success=true, offlineUuid, username) +--> m_authState = eAuth_WaitingAck ``` In offline mode, the server generates the UUID itself from the username. The client-sent UUID is ignored, preventing UUID forgery. --- ## State Machine (Server - PendingConnection) ``` eAuth_None | +--> sendPreLoginResponse() sends AuthSchemePacket | v eAuth_WaitingResponse | +--> handleAuthResponse() received | v eAuth_Verifying (only for "mojang" scheme) | +--> tick() detects result ready | v eAuth_WaitingAck | +--> handleLogin() received | v eAuth_Done | +--> handleLogin() proceeds normally ``` If a `LoginPacket` arrives in state `eAuth_None`, `eAuth_WaitingResponse`, or `eAuth_Verifying`, the connection is closed with `eDisconnect_AuthFailed`. --- ## Channel Security The game protocol is **plaintext** — no AES, no TLS on the game socket. `SharedKeyPacket` (vanilla Java's encryption setup) is disabled. This is fine because no secrets travel over the game channel. What travels on the game socket (plaintext): - UUID, username, skin PNG, serverId challenge, gamestate What **never** travels on the game socket: - Access tokens, refresh tokens, session tokens The Mojang access token stays local on the client. It's sent only to `sessionserver.mojang.com` via HTTPS (`WINHTTP_FLAG_SECURE` in `MCAuthHttp.cpp`). The game server never sees it — it verifies the player by calling `HasJoined` on Mojang's side, also over HTTPS. The `serverId` challenge is random per connection (20 hex chars, generated in `PendingConnection`). A captured serverId is useless — Mojang consumes the `(username, serverId)` binding on the first `JoinServer` call. Replay is not possible. On console platforms (Xbox/QNet, PS/SQR), the transport layer provides its own integrity. On Windows64 it's raw TCP — an attacker on the same network can see gamestate (positions, chat, inventory), but that's a cheat/sniffing concern, not an auth one. Same situation as vanilla Java pre-1.7. --- ## Mojang Skin Handling The server downloads skins and distributes them inline: 1. Server downloads the skin during verification (background thread) 2. AuthResultPacket contains the raw PNG bytes 3. Client registers the texture in memory via `app.AddMemoryTextureFile()` 4. Server distributes the skin to all connected clients via `TextureAndGeometryPacket` 5. New clients receive existing skins at join time --- ## MCAuth Library The authentication backend lives in `MCAuth/`. ### JavaAuthManager - Login via Device Code Flow (OAuth 2.0) — user visits `microsoft.com/link` - Automatic token refresh - Token save/load to disk (JSON) ### Verification Functions | Function | Usage | |---|---| | `JoinServer()` | Client: POST to sessionserver to announce join | | `HasJoined()` | Server: GET to sessionserver to verify identity | | `FetchSkinPng()` | Downloads skin PNG from CDN | | `FetchProfileSkinUrl()` | Gets skin URL from Mojang profile | | `ValidateSkinPng()` | Validates PNG (magic bytes + 64x32 or 64x64 dimensions) | ### UUID Utilities | Function | Usage | |---|---| | `GenerateOfflineUuid()` | UUID v3 from username (vanilla-compatible) | | `DashUuid()` / `UndashUuid()` | UUID format conversion | | `MakeSkinKey()` | Generates memory texture key (e.g. `"mojang_skin_069a79f4.png"`) | ### MCAuthManager (Singleton) - Multi-account with splitscreen support (up to `XUSER_MAX_COUNT` slots) - Each slot has its own `JavaAuthManager` and independent session - Background workers: detached threads capturing `shared_ptr`, invalidated by generation counter - Cancellation via `RequestCancel()` - Account persistence in `java_accounts.json` --- ## Thread Safety ### AuthVerifyResult (PendingConnection) ```cpp struct AuthVerifyResult { std::atomic ready{false}; std::atomic cancelled{false}; std::mutex mutex; bool success; std::string username, uuid, skinUrl; std::vector skinData; }; ``` - Background thread writes under mutex, then `ready.store(true, memory_order_release)` - Main thread checks `ready` with `memory_order_acquire`, then reads under mutex - `cancelled` flag lets PendingConnection destructor abort unnecessary work - `shared_ptr` prevents use-after-free if PendingConnection is destroyed before the thread finishes ### MCAuthManager Slots - Each slot has its own mutex - Atomic `generation` counter invalidates stale workers - Workers capture `shared_ptr` by value — if the slot is reset, the old engine stays alive until the thread exits --- ## Files ### Packets (Minecraft.World/) | File | Description | |---|---| | `AuthSchemePacket.h/.cpp` | Scheme offer packet | | `AuthResponsePacket.h/.cpp` | Client response packet | | `AuthResultPacket.h/.cpp` | Result packet + skin data | | `DisconnectPacket.h` | `eDisconnect_AuthFailed = 31` | | `PacketListener.h` | Auth packet handlers | | `GameUUID.h/.cpp` | UUID utilities (fromDashed, generateOffline) | ### Server (Minecraft.Client/) | File | Description | |---|---| | `PendingConnection.h/.cpp` | Auth state machine, Mojang verification, skin distribution | | `PlayerList.cpp` | Ban check against auth UUID | ### Client (Minecraft.Client/) | File | Description | |---|---| | `ClientConnection.h/.cpp` | handleAuthScheme, handleAuthResult, sendLoginPacketAfterAuth | | `UIScene_MSAuth.h/.cpp` | Account management UI (list, device code, offline) | ### Auth Library (MCAuth/) | File | Description | |---|---| | `MCAuth.h` | Public API: login, verification, skin, UUID | | `MCAuthManager.h` | Multi-account singleton with splitscreen support | | `MCAuth.cpp` | UUID utilities, skin validation | | `MCAuthJava.cpp` | Java Edition auth flow (MSA -> XBL -> Mojang) | | `MCAuthElyby.cpp` | Ely.by Yggdrasil auth (login, refresh, validate, token persistence) | | `MCAuthSession.cpp` | Session verification (JoinServer, HasJoined, Elyby variants) | | `MCAuthHttp.cpp/.h` | HTTP client (WinHTTP) | | `MCAuthCrypto.cpp/.h` | Base64, SHA, JSON parsing | | `MCAuthPlatform.cpp` | File paths, persistence |