#pragma once /* * MCAuth - Microsoft/Xbox Live Authentication library for Minecraft Java Edition * C++ port of the MinecraftAuth Java library by RaphiMC * * Usage: * MCAuth::JavaAuthManager auth; * MCAuth::JavaSession session; * std::string error; * * bool ok = auth.Login( * [](const MCAuth::DeviceCodeInfo& dc) { * // Show dc.userCode and dc.directUri to the user * }, * session, error * ); */ #include #include #include #include #include namespace MCAuth { // ----- Error codes ----- enum class AuthErrorCode { None = 0, NetworkTimeout, // connection timed out or DNS failure NetworkError, // general HTTP/TLS failure (send/receive) HttpError, // unexpected HTTP status code InvalidCredentials, // wrong token, expired refresh token, account not authorised TokenExpired, // access token expired (needs refresh) ServerUnavailable, // Mojang/MS service returned 5xx RateLimited, // HTTP 429 ProfileNotOwned, // MS account exists but doesn't own MC Java Edition Cancelled, // operation was cancelled (e.g. device code timeout) InternalError, // unexpected exception / crypto failure }; struct AuthError { AuthErrorCode code = AuthErrorCode::None; std::string message; int httpStatus = 0; // 0 if not applicable bool ok() const { return code == AuthErrorCode::None; } explicit operator bool() const { return !ok(); } // true means error }; // ----- Constants ---- // Java Edition title ID (Win32, used for SISU auth flow) static constexpr const char* JAVA_CLIENT_ID = "00000000402b5328"; static constexpr const char* MSA_SCOPE = "service::user.auth.xboxlive.com::MBI_SSL"; // ----- Public data types ----- struct DeviceCodeInfo { std::string userCode; // e.g. "ABCD1234" std::string verificationUri; // e.g. "https://www.microsoft.com/link" std::string directUri; // verificationUri + "?otc=" + userCode std::string deviceCode; // internal polling token int64_t expiresMs; // absolute timestamp (ms since epoch) int64_t intervalMs; // polling interval in ms }; using DeviceCodeCallback = std::function; struct JavaSession { std::string username; // Minecraft username (from /minecraft/profile) std::string uuid; // Player UUID (dashed, e.g. "069a79f4-44e9-4726-a5be-fca90e38aaf5") std::string accessToken; // Full Authorization header value ("Bearer ") int64_t expireMs; // When the access token expires (ms since epoch) }; // ----- Java Edition auth manager ----- class JavaAuthManager { public: JavaAuthManager(); ~JavaAuthManager(); JavaAuthManager(const JavaAuthManager&) = delete; JavaAuthManager& operator=(const JavaAuthManager&) = delete; // Synchronous device-code login (blocks until complete or timeout). bool Login(DeviceCodeCallback onDeviceCode, JavaSession& outSession, std::string& error, int timeoutSeconds = 300); // Refresh using the saved MSA refresh token. bool Refresh(JavaSession& outSession, std::string& error); bool IsLoggedIn() const; void Logout(); // Signal a blocking Login() to abort as soon as possible. // Thread-safe — can be called from any thread while Login() is running. void RequestCancel(); bool SaveTokens(const std::string& filePath) const; bool LoadTokens(const std::string& filePath); private: struct Impl; std::unique_ptr m_impl; }; // ----- Ely.by Yggdrasil auth (free functions, no class) ----- struct ElybyTokens { std::string accessToken; std::string clientToken; std::string uuid; // undashed std::string username; }; // POST https://authserver.ely.by/auth/authenticate // On 2FA requirement, error is set to "elyby_2fa_required"; caller retries with password = "pass:totp_code" bool ElybyLogin(const std::string& username, const std::string& password, ElybyTokens& outTokens, std::string& error); // POST https://authserver.ely.by/auth/refresh bool ElybyRefresh(ElybyTokens& tokens, std::string& error); // POST https://authserver.ely.by/auth/validate bool ElybyValidate(const std::string& accessToken, std::string& error); // Token persistence — simple JSON: {accessToken, clientToken, uuid, username} bool ElybyLoadTokens(const std::string& path, ElybyTokens& out); bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens); // ----- Skin key utility ----- // Build the canonical memory-texture key for a Mojang skin. // uuid can be dashed or undashed — dashes are stripped automatically. // Returns e.g. "mojang_skin_069a79f444e94726a5befca90e38aaf5.png" std::string MakeSkinKey(const std::string& uuid); // ----- UUID utilities ----- // Remove dashes from a UUID string: "069a79f4-44e9-..." → "069a79f444e9..." std::string UndashUuid(const std::string& dashed); // Insert dashes into a 32-char hex UUID: "069a79f444e9..." → "069a79f4-44e9-..." std::string DashUuid(const std::string& undashed); // Generate a Minecraft-compatible offline UUID (UUID v3, namespace "OfflinePlayer:" + username). // Returns dashed format. std::string GenerateOfflineUuid(const std::string& username); // Parse a dashed UUID into two 64-bit halves (big-endian). struct Uuid128 { uint64_t hi = 0; uint64_t lo = 0; bool isValid() const { return hi != 0 || lo != 0; } }; Uuid128 ParseUuid128(const std::string& dashed); // ----- Session verification (Mojang sessionserver) ----- // Client-side: call BEFORE connecting to an online-mode server. // Posts to sessionserver.mojang.com/session/minecraft/join. // accessToken = raw token (without "Bearer " prefix). // undashedUuid = 32-char hex UUID of the player. // serverId = the server's challenge string. // Returns true on success (HTTP 204). bool JoinServer(const std::string& accessToken, const std::string& undashedUuid, const std::string& serverId, std::string& error); // Server-side result from HasJoined verification. struct HasJoinedResult { bool success = false; AuthError error; // structured error (code + message + httpStatus) std::string username; // verified username from Mojang std::string uuid; // undashed UUID std::string skinUrl; // Mojang skin texture URL (from base64 properties) std::string capeUrl; // Mojang cape texture URL (from base64 properties) }; // Server-side: verify that a player has called /join. // GETs sessionserver.mojang.com/session/minecraft/hasJoined?username=X&serverId=Y. // Returns success=true with username+uuid on HTTP 200, success=false on 204 (not joined). HasJoinedResult HasJoined(const std::string& username, const std::string& serverId, std::string& error); // Download a skin/cape PNG from a URL. // Returns the raw PNG bytes, or empty vector on failure. std::vector FetchSkinPng(const std::string& url, std::string& error); // Fetch the skin URL for a player UUID from Mojang's session server. // uuid can be dashed or undashed. // Returns the skin texture URL, or empty string on failure. std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error); // Download a skin PNG and return the raw bytes WITHOUT cropping to 64x32. // Use this when you need the full 64x64 texture (e.g. for rendering the head). std::vector FetchSkinPngRaw(const std::string& url, std::string& error); // ----- Ely.by session functions (same Yggdrasil protocol, different URLs) ----- bool ElybyJoinServer(const std::string& accessToken, const std::string& undashedUuid, const std::string& serverId, std::string& error); HasJoinedResult ElybyHasJoined(const std::string& username, const std::string& serverId, std::string& error); std::string ElybyFetchProfileSkinUrl(const std::string& uuid, std::string& error); std::string MakeElybySkinKey(const std::string& uuid); // ----- Skin validation ----- // Maximum allowed skin PNG size in bytes (32KB — a 64x64 RGBA PNG is ~4KB compressed) static constexpr size_t kMaxSkinBytes = 32768; // Check PNG magic, size cap, and 64x32 or 64x64 dimensions. bool ValidateSkinPng(const uint8_t* data, size_t size); } // namespace MCAuth