From 4822dfdc9642629f69c17896b34ad2388641f88f Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Sun, 27 Jul 2025 18:19:06 -0400 Subject: [PATCH 1/4] Allow specifying authlib-injector API path The previous authlib-injector authentication implementation always assumed that the authlib-injector API location was located at https://authlibinjectorserver.example.com/api/yggdrasil. Blessing Skin structures their API like that, but other authlib-injector-compatible Yggdrasil implementations do not. Per the authlib-injector specification, the API root can be located at any path, and that path should be pointed to by the `X-Authlib-Injector-API-Location` header [1]. With this change, the default AuthlibInjectorAPIPath is kept as `/api/yggdrasil`, so users will not need to update their config. See also https://github.com/unmojang/drasl/issues/66#issuecomment-2081817705. [1] https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83 --- MinecraftClient/Protocol/ProtocolHandler.cs | 8 ++-- .../ConfigComments/ConfigComments.resx | 13 +++++- MinecraftClient/Settings.cs | 43 +++++++++++-------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index cbf1b1cd90..00910d62b7 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -603,7 +603,8 @@ private static LoginResult YggdrasiLogin(string user, string pass, out SessionTo JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) + "\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }"; int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port, - "/api/yggdrasil/authserver/authenticate", json_request, ref result); + Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/authenticate", + json_request, ref result); if (code == 200) { if (result.Contains("availableProfiles\":[]}")) @@ -914,7 +915,8 @@ public static LoginResult GetNewYggdrasilToken(SessionToken currentsession, out "\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) + "\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }"; int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port, - "/api/yggdrasil/authserver/refresh", json_request, ref result); + Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/refresh", + json_request, ref result); if (code == 200) { if (result == null) @@ -974,7 +976,7 @@ public static bool SessionCheck(string uuid, string accesstoken, string serverha : "sessionserver.mojang.com"; int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443; string endpoint = type == LoginType.yggdrasil - ? "/api/yggdrasil/sessionserver/session/minecraft/join" + ? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join" : "/session/minecraft/join"; int code = DoHTTPSPost(host, port, endpoint, json_request, ref result); diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index ca44031449..531abc0c18 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -850,9 +850,18 @@ If the connection to the Minecraft game server is blocked by the firewall, set E Ignore invalid player name - Yggdrasil authlib server domain name and port. + authlib-injector authentication server to use for Yggdrasil accounts + + + Domain name or IP address + + + Port to connect on + + + Path component of the authlib-injector API location. Refer to the authlib-injector documentation for more info. Set to false to opt-out of Sentry error logging. - \ No newline at end of file + diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 684e5a98de..3e9f0b66ba 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -494,9 +494,9 @@ public class GeneralConfig [TomlInlineComment("$Main.General.method$")] public LoginMethod Method = LoginMethod.mcc; + [TomlInlineComment("$Main.General.AuthlibServer$")] - public AuthlibServer AuthServer = new(string.Empty); - + public AuthlibServer AuthServer = new(); public enum LoginType { mojang, microsoft,yggdrasil }; @@ -694,28 +694,37 @@ public ServerInfoConfig(string Host, ushort Port) this.Port = Port; } } - public struct AuthlibServer + + [TomlDoNotInlineObject] + public class AuthlibServer { - public string Host = string.Empty; - public int Port = 443; + [NonSerialized] + private string _host = string.Empty; - public AuthlibServer(string Host) + [TomlInlineComment("$AuthlibServer.Host$")] + public string Host { - string[] sip = Host.Split(new[] { ":", ":" }, StringSplitOptions.None); - this.Host = sip[0]; - - if (sip.Length > 1) + get => _host; + set { - try { this.Port = Convert.ToUInt16(sip[1]); } - catch (FormatException) { } + string[] split = value.Split(new[] { ":", ":" }, StringSplitOptions.None); + if (split.Length >= 1) + { + _host = split[0]; + } + if (split.Length >= 2) + { + try { Port = Convert.ToUInt16(split[1]); } + catch (FormatException) { } + } } } - public AuthlibServer(string Host, ushort Port) - { - this.Host = Host.Split(new[] { ":", ":" }, StringSplitOptions.None)[0]; - this.Port = Port; - } + [TomlInlineComment("$AuthlibServer.Port$")] + public int Port = 443; + + [TomlInlineComment("$AuthlibServer.AuthlibInjectorAPIPath$")] + public string AuthlibInjectorAPIPath = "/api/yggdrasil"; } } } From 54c701e4ef48d2de70c42034dbf2f1e159b45098 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Sun, 27 Jul 2025 22:26:10 +0000 Subject: [PATCH 2/4] yggdrasil: Don't send invalid profile key signature Instead of sending an invalid profile key signature, it's better to just send no signature. If we send an invalid signature, the vanilla server will throw an error even if `enforce-secure-profile` is `false`. See also https://github.com/yushijinhun/authlib-injector/pull/266. --- .../Protocol/ProfileKey/KeyUtils.cs | 52 +++++-------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index 708e99d969..a4779489b1 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -16,29 +16,26 @@ static class KeyUtils public static PlayerKeyPair? GetNewProfileKeys(string accessToken, bool isYggdrasil) { + if (isYggdrasil) + return null; + ProxiedWebRequest.Response? response = null; try { - if (!isYggdrasil) + var request = new ProxiedWebRequest(certificates) { - var request = new ProxiedWebRequest(certificates) - { - Accept = "application/json" - }; - request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + Accept = "application/json" + }; + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); - response = request.Post("application/json", ""); + response = request.Post("application/json", ""); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(response.Body.ToString()); - } + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(response.Body.ToString()); } - // see https://github.com/yushijinhun/authlib-injector/blob/da910956eaa30d2f6c2c457222d188aeb53b0d1f/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java#L49 - // POST to "https://api.minecraftservices.com/player/certificates" with authlib-injector will get a dummy response - Json.JSONData json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body); - // Error here + Json.JSONData json = Json.ParseJson(response!.Body); PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue, sig: json.Properties["publicKeySignature"].StringValue, sigV2: json.Properties["publicKeySignatureV2"].StringValue); @@ -234,30 +231,5 @@ public static string EscapeString(string src) sb.Append(src, start, src.Length - start); return sb.ToString(); } - - public static Json.JSONData MakeDummyResponse() - { - RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048); - var mimePublicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); - var mimePrivateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); - string publicKeyPEM = $"-----BEGIN RSA PUBLIC KEY-----\n{mimePublicKey}\n-----END RSA PUBLIC KEY-----\n"; - string privateKeyPEM = $"-----BEGIN RSA PRIVATE KEY-----\n{mimePrivateKey}\n-----END RSA PRIVATE KEY-----\n"; - DateTime now = DateTime.UtcNow; - DateTime expiresAt = now.AddHours(48); - DateTime refreshedAfter = now.AddHours(36); - Json.JSONData response = new(Json.JSONData.DataType.Object); - Json.JSONData keyPairObj = new(Json.JSONData.DataType.Object); - keyPairObj.Properties["privateKey"] = new(Json.JSONData.DataType.String){ StringValue = privateKeyPEM }; - keyPairObj.Properties["publicKey"] = new(Json.JSONData.DataType.String){ StringValue = publicKeyPEM }; - - response.Properties["keyPair"] = keyPairObj; - response.Properties["publicKeySignature"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" }; - response.Properties["publicKeySignatureV2"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" }; - string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; - response.Properties["expiresAt"] = new(Json.JSONData.DataType.String){ StringValue = expiresAt.ToString(format) }; - response.Properties["refreshedAfter"] = new(Json.JSONData.DataType.String){ StringValue = refreshedAfter.ToString(format) }; - - return response; - } } } From ab94b03cf50e5763a9f126ecacfba8cf60905977 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Mon, 28 Jul 2025 00:17:49 -0400 Subject: [PATCH 3/4] Use System.Net.Http.HttpClient for DoHTTPSRequest The DIY HTTP client had many problems, as is expected when rolling your own HTTP implementation without following the spec. Fortunately, we can use SocketsHttpHandler.ConnectCallback to make HttpClient use our custom TCP client. --- MinecraftClient/Protocol/ProtocolHandler.cs | 109 ++++++++++---------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 00910d62b7..a49c09e6f7 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -3,6 +3,7 @@ using System.Data.Odbc; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; @@ -975,11 +976,11 @@ public static bool SessionCheck(string uuid, string accesstoken, string serverha ? Config.Main.General.AuthServer.Host : "sessionserver.mojang.com"; int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443; - string endpoint = type == LoginType.yggdrasil + string path = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join" : "/session/minecraft/join"; - int code = DoHTTPSPost(host, port, endpoint, json_request, ref result); + int code = DoHTTPSPost(host, port, path, json_request, ref result); return (code >= 200 && code < 300); } catch @@ -1106,22 +1107,16 @@ public static string GetRealmsWorldServerAddress(string worldId, string username /// Cookies for making the request /// Request result /// HTTP Status code - private static int DoHTTPSGet(string host, int port, string endpoint, string cookies, ref string result) + private static int DoHTTPSGet(string host, int port, string path, string cookies, ref string result) { - List http_request = new() + Dictionary headers = new() { - "GET " + endpoint + " HTTP/1.1", - "Cookie: " + cookies, - "Cache-Control: no-cache", - "Pragma: no-cache", - "Host: " + host, - "User-Agent: Java/1.6.0_27", - "Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7", - "Connection: close", - "", - "" + { "Cookie", cookies }, + { "Cache-Control", "no-cache" }, + { "Pragma", "no-cache" }, + { "User-Agent", "Java/1.6.0_27" } }; - return DoHTTPSRequest(http_request, host, port, ref result); + return DoHTTPSRequest(HttpMethod.Get, host, port, path, headers, null, ref result); } /// @@ -1132,31 +1127,24 @@ private static int DoHTTPSGet(string host, int port, string endpoint, string coo /// Request payload /// Request result /// HTTP Status code - private static int DoHTTPSPost(string host, int port, string endpoint, string request, ref string result) + private static int DoHTTPSPost(string host, int port, string path, string body, ref string result) { - List http_request = new() + Dictionary headers = new() { - "POST " + endpoint + " HTTP/1.1", - "Host: " + host, - "User-Agent: MCC/" + Program.Version, - "Content-Type: application/json", - "Content-Length: " + Encoding.ASCII.GetBytes(request).Length, - "Connection: close", - "", - request + { "User-Agent", "MCC/" + Program.Version }, + { "Content-Type", "application/json" } }; - return DoHTTPSRequest(http_request, host, port, ref result); + return DoHTTPSRequest(HttpMethod.Post, host, port, path, headers, body, ref result); } /// - /// Manual HTTPS request since we must directly use a TcpClient because of the proxy. - /// This method connects to the server, enables SSL, do the request and read the response. + /// This method connects to the server, enables TLS, does the request, and reads the response. /// /// Request headers and optional body (POST) /// Host to connect to /// Request result /// HTTP Status code - private static int DoHTTPSRequest(List headers, string host, int port, ref string result) + private static int DoHTTPSRequest(HttpMethod method, string host, int port, string path, Dictionary headers, string? body, ref string result) { string? postResult = null; int statusCode = 520; @@ -1167,41 +1155,52 @@ private static int DoHTTPSRequest(List headers, string host, int port, r { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host)); + + using SocketsHttpHandler handler = new SocketsHttpHandler(); + handler.ConnectCallback = async (ctx, ct) => + { + TcpClient client = ProxyHandler.NewTcpClient(host, port, true); + return client.GetStream(); + }; - TcpClient client = ProxyHandler.NewTcpClient(host, port, true); - SslStream stream = new(client.GetStream()); - stream.AuthenticateAsClient(host, null, SslProtocols.Tls12, - true); // Enable TLS 1.2. Hotfix for #1780 + using HttpClient client = new HttpClient(handler); - if (Settings.Config.Logging.DebugMessages) - foreach (string line in headers) - ConsoleIO.WriteLineFormatted("§8> " + line); + var request = new HttpRequestMessage(method, "https://" + host + ":" + port + path); - stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); - System.IO.StreamReader sr = new(stream); - string raw_result = sr.ReadToEnd(); + var contentType = "text/plain"; + foreach (var header in headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) + contentType = header.Value; + } + + if (body != null) + { + request.Content = new StringContent(body, Encoding.UTF8, contentType); + } if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8> " + request); + + HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult(); + statusCode = (int)(response.StatusCode); + if (statusCode == 204) { - ConsoleIO.WriteLine(""); - foreach (string line in raw_result.Split('\n')) - ConsoleIO.WriteLineFormatted("§8< " + line); + postResult = "No Content"; + } + else + { + postResult = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + } - if (raw_result.StartsWith("HTTP/1.1")) + if (Settings.Config.Logging.DebugMessages) { - statusCode = int.Parse(raw_result.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); - if (statusCode != 204) - { - var splited = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..].Split("\r\n"); - postResult = splited[1] + splited[3]; - } - else - { - postResult = "No Content"; - } + ConsoleIO.WriteLine(""); + foreach (string line in postResult.Split('\n')) + ConsoleIO.WriteLineFormatted("§8< " + line); } - else statusCode = 520; //Web server is returning an unknown error } catch (Exception e) { @@ -1258,4 +1257,4 @@ public static DateTime UnixTimeStampToDateTime(long unixTimeStamp) return dateTime; } } -} \ No newline at end of file +} From 5f252620c159ca84f49e8fff9c02c3f5037efd5c Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 31 Jul 2025 01:57:36 +0000 Subject: [PATCH 4/4] Fetch certificates for Yggdrasil accounts Some authlib-injector-compatible authentication servers implement the POST /player/certificates route used for fetching player certificates. To comply with the authlib-injector Yggdrasil server specification [1], we first query the authlib-injector metadata and check for the `feature.enable_profile_key` flag. If the flag is set, the authentication server supports the /player/certificates route. In this case, checking the authlib-injector metadata isn't really necessary; if the /player/certificates request fails for any reason, we simply ignore it anyway. [1] https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83 --- .../Protocol/ProfileKey/KeyUtils.cs | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index a4779489b1..ef214f83fe 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -12,17 +12,73 @@ static class KeyUtils { private static readonly SHA256 sha256Hash = SHA256.Create(); - private static readonly string certificates = "https://api.minecraftservices.com/player/certificates"; + public static bool AuthServerSupportsProfileKeys(bool isYggdrasil) + { + // Check whether the authentication server supports player profile keys + if (!isYggdrasil) + return true; + + ProxiedWebRequest.Response? response = null; + try + { + var authServer = Settings.Config.Main.General.AuthServer; + var request = new ProxiedWebRequest( + "https://" + authServer.Host + ":" + authServer.Port + authServer.AuthlibInjectorAPIPath) + { + Accept = "application/json" + }; + + response = request.Get(); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(response.Body.ToString()); + } + + // The feature.enable_profile_key flag is documented at + // https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83 + Json.JSONData json = Json.ParseJson(response.Body); + bool enableProfileKey = json.Properties["meta"].Properties.ContainsKey("feature.enable_profile_key") && + json.Properties["meta"].Properties["feature.enable_profile_key"].StringValue == "true"; + if (enableProfileKey) + { + return true; + } + } + catch (Exception e) + { + int code = response == null ? 0 : response.StatusCode; + ConsoleIO.WriteLineFormatted("§cFetch authlib-injector metadata failed: HttpCode = " + code + ", Error = " + e.Message); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + } + return false; + } public static PlayerKeyPair? GetNewProfileKeys(string accessToken, bool isYggdrasil) { - if (isYggdrasil) + if (!AuthServerSupportsProfileKeys(isYggdrasil)) + { + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine("AuthServer does not support profile keys, will not attempt to fetch them."); + } return null; + } + + string certificatesURL = "https://api.minecraftservices.com/player/certificates"; + if (isYggdrasil) + { + var authServer = Settings.Config.Main.General.AuthServer; + certificatesURL = "https://" + authServer.Host + ":" + authServer.Port + + authServer.AuthlibInjectorAPIPath + "/minecraftservices/player/certificates"; + } ProxiedWebRequest.Response? response = null; try { - var request = new ProxiedWebRequest(certificates) + var request = new ProxiedWebRequest(certificatesURL) { Accept = "application/json" };