diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs
index 7da5bcfc..e5bf6572 100644
--- a/MinecraftClient/Program.cs
+++ b/MinecraftClient/Program.cs
@@ -58,6 +58,7 @@ namespace MinecraftClient
private static int offlinePromptActive;
private static int exitOnFailurePending;
private static string settingsIniPath = "MinecraftClient.ini";
+ private static AuthenticationSelection? pendingAuthenticationSelection;
// [SENTRY]
// Setting this string to an empty string will disable Sentry
@@ -565,15 +566,19 @@ namespace MinecraftClient
// Setup exit cleaning code
ExitCleanUp.Add(() => { DoExit(); });
+ if (HasNoConfiguredLoginDetails())
+ {
+ if (!PromptForAuthenticationSelection())
+ return;
+ }
+
//Asking the user to type in missing data such as Username and Password
bool useBrowser = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser;
bool useDeviceCode = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.mcc;
bool skipPassword = useBrowser || useDeviceCode;
- if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser)
+ if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !skipPassword)
{
- ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
- InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
- if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
+ if (!RequestLogin())
{
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
return;
@@ -603,6 +608,145 @@ namespace MinecraftClient
InternalConfig.Account.Password = password;
}
+ private static bool HasNoConfiguredLoginDetails()
+ => string.IsNullOrWhiteSpace(InternalConfig.Account.Login)
+ && string.IsNullOrWhiteSpace(InternalConfig.Account.Password);
+
+ private static bool PromptForAuthenticationSelection()
+ {
+ while (true)
+ {
+ ConsoleIO.WriteLine(Translations.mcc_auth_method_prompt);
+ string selection = ConsoleIO.ReadLine().Trim();
+
+ switch (selection.ToLowerInvariant())
+ {
+ case "1":
+ case "offline":
+ BeginAuthenticationSelection(LoginType.mojang, LoginMethod.mcc);
+ if (!RequestLogin())
+ {
+ DiscardAuthenticationSelection();
+ HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
+ return false;
+ }
+
+ InternalConfig.Account.Password = "-";
+ return true;
+
+ case "2":
+ case "online":
+ case "microsoft":
+ BeginAuthenticationSelection(LoginType.microsoft, LoginMethod.mcc);
+ return true;
+
+ case "3":
+ case "yggdrasil":
+ BeginAuthenticationSelection(LoginType.yggdrasil, LoginMethod.mcc);
+ if (!RequestLogin() || !RequestRequiredPassword())
+ {
+ DiscardAuthenticationSelection();
+ HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
+ return false;
+ }
+
+ if (!RequestAuthlibServer())
+ {
+ DiscardAuthenticationSelection();
+ return false;
+ }
+
+ return true;
+
+ default:
+ ConsoleIO.WriteLine(Translations.mcc_auth_method_invalid);
+ break;
+ }
+ }
+ }
+
+ private static void BeginAuthenticationSelection(LoginType accountType, LoginMethod method)
+ {
+ pendingAuthenticationSelection ??= new AuthenticationSelection(
+ Config.Main.General.AccountType,
+ Config.Main.General.Method,
+ Config.Main.General.AuthServerUrl);
+
+ Config.Main.General.AccountType = accountType;
+ Config.Main.General.Method = method;
+ }
+
+ private static bool RequestLogin()
+ {
+ ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
+ InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
+ return !string.IsNullOrWhiteSpace(InternalConfig.Account.Login);
+ }
+
+ private static bool RequestRequiredPassword()
+ {
+ ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden);
+ string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword();
+ if (string.IsNullOrWhiteSpace(password))
+ return false;
+
+ InternalConfig.Account.Password = password;
+ return true;
+ }
+
+ private static bool RequestAuthlibServer()
+ {
+ while (true)
+ {
+ ConsoleIO.WriteLine(Translations.mcc_yggdrasil_url);
+ string authServerUrl = ConsoleIO.ReadLine().Trim();
+ if (!Config.Main.General.TrySetAuthServerUrl(authServerUrl)
+ || !Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
+ {
+ ConsoleIO.WriteLine(Translations.mcc_yggdrasil_invalid_url);
+ continue;
+ }
+
+ switch (ProtocolHandler.ValidateAuthlibServer(authServerUri))
+ {
+ case ProtocolHandler.AuthlibServerValidationResult.Valid:
+ return true;
+ case ProtocolHandler.AuthlibServerValidationResult.Unreachable:
+ ConsoleIO.WriteLine(Translations.mcc_yggdrasil_server_unreachable);
+ break;
+ default:
+ ConsoleIO.WriteLine(Translations.mcc_yggdrasil_server_invalid);
+ break;
+ }
+ }
+ }
+
+ private static void PersistAuthenticationSelection(SessionToken session)
+ {
+ if (pendingAuthenticationSelection is null)
+ return;
+
+ if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
+ InternalConfig.Account.Login = session.PlayerName;
+
+ Config.Main.General.Account = InternalConfig.Account;
+ WriteBackSettings();
+ pendingAuthenticationSelection = null;
+ }
+
+ private static void DiscardAuthenticationSelection()
+ {
+ if (pendingAuthenticationSelection is not AuthenticationSelection selection)
+ return;
+
+ Config.Main.General.AccountType = selection.AccountType;
+ Config.Main.General.Method = selection.Method;
+ Config.Main.General.AuthServerUrl = selection.AuthServerUrl;
+ pendingAuthenticationSelection = null;
+ }
+
+ private sealed record AuthenticationSelection(LoginType AccountType, LoginMethod Method, string AuthServerUrl);
+
///
/// Start a new Client
///
@@ -664,17 +808,26 @@ namespace MinecraftClient
if (result != ProtocolHandler.LoginResult.Success)
{
- ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServer.Host)));
+ ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServerUrl)));
result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session);
}
- if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none)
- SessionCache.Store(loginLower, session);
+ if (result == ProtocolHandler.LoginResult.Success)
+ {
+ PersistAuthenticationSelection(session);
+ loginLower = ToLowerIfNeed(InternalConfig.Account.Login);
+
+ if (Config.Main.Advanced.SessionCache != CacheType.none)
+ SessionCache.Store(loginLower, session);
+ }
if (result == ProtocolHandler.LoginResult.Success)
session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck(Config.Main.General.AccountType));
}
+ if (result == ProtocolHandler.LoginResult.Success)
+ PersistAuthenticationSelection(session);
+
if (result == ProtocolHandler.LoginResult.Success)
{
InternalConfig.Username = session.PlayerName;
@@ -859,6 +1012,7 @@ namespace MinecraftClient
}
else
{
+ DiscardAuthenticationSelection();
string failureMessage = Translations.error_login;
string failureReason = result switch
{
diff --git a/MinecraftClient/Protocol/MicrosoftAuthentication.cs b/MinecraftClient/Protocol/MicrosoftAuthentication.cs
index 4c84ce47..e91806ba 100644
--- a/MinecraftClient/Protocol/MicrosoftAuthentication.cs
+++ b/MinecraftClient/Protocol/MicrosoftAuthentication.cs
@@ -153,7 +153,7 @@ namespace MinecraftClient.Protocol
// Extract email from JWT id_token
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
var jsonPayload = Json.ParseJson(payload);
- string email = jsonPayload!["email"]!.GetStringValue();
+ string email = jsonPayload?["email"]?.GetStringValue() ?? string.Empty;
return new LoginResponse()
{
@@ -195,7 +195,7 @@ namespace MinecraftClient.Protocol
// Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
var jsonPayload = Json.ParseJson(payload);
- string email = jsonPayload!["email"]!.GetStringValue();
+ string email = jsonPayload?["email"]?.GetStringValue() ?? string.Empty;
return new LoginResponse()
{
Email = email,
diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs
index 381af81f..ca19e243 100644
--- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs
+++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs
@@ -26,9 +26,10 @@ namespace MinecraftClient.Protocol.ProfileKey
ProxiedWebRequest.Response? response = null;
try
{
- var authServer = Settings.Config.Main.General.AuthServer;
- var request = new ProxiedWebRequest(
- (authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port + authServer.AuthlibInjectorAPIPath)
+ if (!Settings.Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
+ return false;
+
+ var request = new ProxiedWebRequest(authServerUri.AbsoluteUri)
{
Accept = "application/json"
};
@@ -66,9 +67,10 @@ namespace MinecraftClient.Protocol.ProfileKey
string certificatesURL = "https://api.minecraftservices.com/player/certificates";
if (isYggdrasil)
{
- var authServer = Settings.Config.Main.General.AuthServer;
- certificatesURL = (authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port +
- authServer.AuthlibInjectorAPIPath + "/minecraftservices/player/certificates";
+ if (!Settings.Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
+ return null;
+
+ certificatesURL = new Uri(authServerUri, "minecraftservices/player/certificates").AbsoluteUri;
}
ProxiedWebRequest.Response? response = null;
diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs
index f231976f..2ebf3ad7 100644
--- a/MinecraftClient/Protocol/ProtocolHandler.cs
+++ b/MinecraftClient/Protocol/ProtocolHandler.cs
@@ -27,6 +27,13 @@ namespace MinecraftClient.Protocol
///
public static class ProtocolHandler
{
+ public enum AuthlibServerValidationResult
+ {
+ Valid,
+ Unreachable,
+ InvalidResponse
+ }
+
///
/// Perform a DNS lookup for a Minecraft Service using the specified domain name
///
@@ -134,6 +141,37 @@ namespace MinecraftClient.Protocol
}
}
+ ///
+ /// Verifies that an authlib-injector URL is reachable and returns its metadata document.
+ ///
+ public static AuthlibServerValidationResult ValidateAuthlibServer(Uri authServerUri)
+ {
+ string result = string.Empty;
+ int statusCode;
+ try
+ {
+ statusCode = DoHTTPSGet(authServerUri, ref result);
+ }
+ catch
+ {
+ return AuthlibServerValidationResult.Unreachable;
+ }
+
+ if (statusCode != 200)
+ return AuthlibServerValidationResult.InvalidResponse;
+
+ try
+ {
+ return Json.ParseJson(result)?["meta"]?["implementationName"]?.GetStringValue() is { Length: > 0 }
+ ? AuthlibServerValidationResult.Valid
+ : AuthlibServerValidationResult.InvalidResponse;
+ }
+ catch
+ {
+ return AuthlibServerValidationResult.InvalidResponse;
+ }
+ }
+
///
/// Get a protocol handler for the specified Minecraft version
///
@@ -714,9 +752,10 @@ namespace MinecraftClient.Protocol
string json_request = "{\"agent\": { \"name\": \"Minecraft\", \"version\": 1 }, \"username\": \"" +
JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) +
"\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }";
- int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port,
- Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/authenticate", json_request,
- Config.Main.General.AuthServer.UseHttps, ref result);
+ if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
+ return LoginResult.OtherError;
+
+ int code = DoHTTPSPost(authServerUri, "authserver/authenticate", json_request, ref result);
if (code == 200)
{
if (result.Contains("availableProfiles\":[]}"))
@@ -922,7 +961,8 @@ namespace MinecraftClient.Protocol
session.PlayerID = profile.UUID;
session.ID = accessToken;
session.RefreshToken = msaResponse.RefreshToken;
- InternalConfig.Account.Login = msaResponse.Email;
+ if (!string.IsNullOrWhiteSpace(msaResponse.Email))
+ InternalConfig.Account.Login = msaResponse.Email;
return LoginResult.Success;
}
else
@@ -1037,9 +1077,10 @@ namespace MinecraftClient.Protocol
"\", \"clientToken\": \"" + JsonEncode(currentsession.ClientID) +
"\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) +
"\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }";
- int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port,
- Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/refresh", json_request,
- Config.Main.General.AuthServer.UseHttps, ref result);
+ if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
+ return LoginResult.OtherError;
+
+ int code = DoHTTPSPost(authServerUri, "authserver/refresh", json_request, ref result);
if (code == 200)
{
if (result is null)
@@ -1093,16 +1134,18 @@ namespace MinecraftClient.Protocol
string result = "";
string json_request = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid +
"\",\"serverId\":\"" + serverhash + "\"}";
- string host = type == LoginType.yggdrasil
- ? Config.Main.General.AuthServer.Host
- : "sessionserver.mojang.com";
- int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443;
- string endpoint = type == LoginType.yggdrasil
- ? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join"
- : "/session/minecraft/join";
+ int code;
+ if (type == LoginType.yggdrasil)
+ {
+ if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
+ return false;
- bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true;
- int code = DoHTTPSPost(host, port, endpoint, json_request, useHttps, ref result);
+ code = DoHTTPSPost(authServerUri, "sessionserver/session/minecraft/join", json_request, ref result);
+ }
+ else
+ {
+ code = DoHTTPSPost("sessionserver.mojang.com", 443, "/session/minecraft/join", json_request, ref result);
+ }
return (code >= 200 && code < 300);
}
catch
@@ -1241,6 +1284,16 @@ namespace MinecraftClient.Protocol
return DoHTTPSRequest(HttpMethod.Get, host, port, path, headers, null, useHttps: true, ref result);
}
+ private static int DoHTTPSGet(Uri requestUri, ref string result)
+ {
+ Dictionary headers = new()
+ {
+ { "User-Agent", "MCC/" + Program.Version }
+ };
+ return DoHTTPSRequest(HttpMethod.Get, requestUri.Host, requestUri.Port, requestUri.PathAndQuery, headers,
+ null, requestUri.Scheme == Uri.UriSchemeHttps, ref result);
+ }
+
///
/// Make a POST request to the specified endpoint of the Mojang API
///
@@ -1253,6 +1306,13 @@ namespace MinecraftClient.Protocol
private static int DoHTTPSPost(string host, int port, string path, string body, ref string result)
=> DoHTTPSPost(host, port, path, body, useHttps: true, ref result);
+ private static int DoHTTPSPost(Uri baseUri, string relativePath, string body, ref string result)
+ {
+ Uri requestUri = new(baseUri, relativePath);
+ return DoHTTPSPost(requestUri.Host, requestUri.Port, requestUri.PathAndQuery, body,
+ requestUri.Scheme == Uri.UriSchemeHttps, ref result);
+ }
+
///
/// Make a POST request to the specified endpoint of the Mojang API
///
@@ -1392,4 +1452,4 @@ namespace MinecraftClient.Protocol
return dateTime;
}
}
-}
\ No newline at end of file
+}
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
index 41c1eee0..5c86e6b0 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
@@ -2011,6 +2011,15 @@ namespace MinecraftClient {
return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path..
+ ///
+ internal static string Main_General_AuthServerUrl {
+ get {
+ return ResourceManager.GetString("Main.General.AuthServerUrl", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Yggdrasil authlib multi-user selection..
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
index 42e3ee0c..f109fd03 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
@@ -983,6 +983,9 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
authlib-injector authentication server to use for Yggdrasil accounts
+
+ Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path.
+
Domain name or IP address
diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs
index 8a0d373e..9ad05c2f 100644
--- a/MinecraftClient/Resources/Translations/Translations.Designer.cs
+++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs
@@ -5998,6 +5998,24 @@ namespace MinecraftClient {
return ResourceManager.GetString("mcc.connecting", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil.
+ ///
+ internal static string mcc_auth_method_prompt {
+ get {
+ return ResourceManager.GetString("mcc.auth_method_prompt", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Please choose 1, 2, or 3..
+ ///
+ internal static string mcc_auth_method_invalid {
+ get {
+ return ResourceManager.GetString("mcc.auth_method_invalid", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Tip: try TUI mode for a cleaner interface, mouse-friendly container actions, and a nicer layout. Run {0}feature tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart..
@@ -6271,6 +6289,114 @@ namespace MinecraftClient {
return ResourceManager.GetString("mcc.password_hidden", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Authlib server host:.
+ ///
+ internal static string mcc_yggdrasil_host {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_host", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Authlib server port [{0}]:.
+ ///
+ internal static string mcc_yggdrasil_port {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_port", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Authlib-Injector API path [{0}]:.
+ ///
+ internal static string mcc_yggdrasil_api_path {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_api_path", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Use HTTPS? [Y/n].
+ ///
+ internal static string mcc_yggdrasil_use_https {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_use_https", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The authlib server host cannot be empty..
+ ///
+ internal static string mcc_yggdrasil_invalid_host {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_invalid_host", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The authlib server port must be between 1 and 65535..
+ ///
+ internal static string mcc_yggdrasil_invalid_port {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_invalid_port", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Please answer yes or no..
+ ///
+ internal static string mcc_yggdrasil_invalid_yes_no {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_invalid_yes_no", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Authlib-injector URL:.
+ ///
+ internal static string mcc_yggdrasil_url {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_url", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The authlib-injector URL must be an absolute HTTP or HTTPS URL..
+ ///
+ internal static string mcc_yggdrasil_invalid_url {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_invalid_url", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Could not reach the authlib-injector server. Check the URL and try again..
+ ///
+ internal static string mcc_yggdrasil_server_unreachable {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_server_unreachable", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The server did not return valid authlib-injector metadata. Check the URL and try again..
+ ///
+ internal static string mcc_yggdrasil_server_invalid {
+ get {
+ return ResourceManager.GetString("mcc.yggdrasil_server_invalid", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The configured authlib-injector URL must be an absolute HTTP or HTTPS URL..
+ ///
+ internal static string config_auth_server_url_invalid {
+ get {
+ return ResourceManager.GetString("config.auth_server_url_invalid", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to You are dead. Type '{0}respawn' to respawn..
diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx
index 0fc9f081..3586a8be 100644
--- a/MinecraftClient/Resources/Translations/Translations.resx
+++ b/MinecraftClient/Resources/Translations/Translations.resx
@@ -2004,6 +2004,27 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
Connecting to {0}...
+
+ Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil
+
+
+ Please choose 1, 2, or 3.
+
+
+ Authlib-injector URL:
+
+
+ The authlib-injector URL must be an absolute HTTP or HTTPS URL.
+
+
+ Could not reach the authlib-injector server. Check the URL and try again.
+
+
+ The server did not return valid authlib-injector metadata. Check the URL and try again.
+
+
+ The configured authlib-injector URL must be an absolute HTTP or HTTPS URL.
+
Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart.
@@ -2097,6 +2118,27 @@ Type '{0}quit' to leave the server.
Password(invisible):
+
+ Authlib server host:
+
+
+ Authlib server port [{0}]:
+
+
+ Authlib-Injector API path [{0}]:
+
+
+ Use HTTPS? [Y/n]
+
+
+ The authlib server host cannot be empty.
+
+
+ The authlib server port must be between 1 and 65535.
+
+
+ Please answer yes or no.
+
You are dead. Type '{0}respawn' to respawn.
diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs
index a5ce88af..c594a336 100644
--- a/MinecraftClient/Settings.cs
+++ b/MinecraftClient/Settings.cs
@@ -209,6 +209,7 @@ namespace MinecraftClient
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
string tomlString = TomletMain.TomlStringFrom(Config);
Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
+ tomlString = RemoveLegacyAuthServerSection(tomlString);
string[] tomlList = tomlString.Split('\n');
StringBuilder newConfig = new();
@@ -272,6 +273,19 @@ namespace MinecraftClient
}
}
+ private static string RemoveLegacyAuthServerSection(string tomlString)
+ {
+ const string legacySection = "[Main.General.AuthServer]";
+ int sectionStart = tomlString.IndexOf(legacySection, StringComparison.Ordinal);
+ if (sectionStart < 0)
+ return tomlString;
+
+ int nextSection = tomlString.IndexOf("\n[", sectionStart + legacySection.Length, StringComparison.Ordinal);
+ return nextSection < 0
+ ? tomlString[..sectionStart]
+ : tomlString.Remove(sectionStart, nextSection - sectionStart + 1);
+ }
+
///
/// Load settings from the command line
///
@@ -656,6 +670,8 @@ namespace MinecraftClient
General.Account.Login ??= string.Empty;
General.Account.Password ??= string.Empty;
+ if (!General.MigrateLegacyAuthServer())
+ ConsoleIO.WriteLogLine(Translations.config_auth_server_url_invalid);
if (!InternalConfig.KeepAccountSettings)
{
if (Advanced.AccountList.TryGetValue(General.Account.Login, out AccountInfoConfig account))
@@ -753,12 +769,72 @@ namespace MinecraftClient
[TomlInlineComment("$Main.General.method$")]
public LoginMethod Method = LoginMethod.mcc;
+
+ [TomlInlineComment("$Main.General.AuthServerUrl$")]
+ public string AuthServerUrl = string.Empty;
+
+ // Retained only to deserialize pre-URL configs. WriteToFile() removes this legacy section.
[TomlInlineComment("$Main.General.AuthlibServer$")]
public AuthlibServer AuthServer = new();
[TomlInlineComment("$Main.General.AuthlibUser$")]
public string AuthUser = "";
+ public bool MigrateLegacyAuthServer()
+ {
+ AuthServerUrl ??= string.Empty;
+ if (TrySetAuthServerUrl(AuthServerUrl))
+ return true;
+
+ if (!string.IsNullOrWhiteSpace(AuthServerUrl))
+ return false;
+
+ if (!string.IsNullOrWhiteSpace(AuthServer.Host))
+ {
+ string path = AuthServer.AuthlibInjectorAPIPath ?? string.Empty;
+ if (!path.StartsWith('/'))
+ path = '/' + path;
+
+ string legacyUrl = $"{(AuthServer.UseHttps ? "https" : "http")}://{AuthServer.Host}:{AuthServer.Port}{path}";
+ return TrySetAuthServerUrl(legacyUrl);
+ }
+
+ return true;
+ }
+
+ public bool TrySetAuthServerUrl(string url)
+ {
+ if (!TryGetNormalizedAuthServerUri(url, out Uri? authServerUri))
+ return false;
+
+ AuthServerUrl = authServerUri.AbsoluteUri;
+ return true;
+ }
+
+ public bool TryGetAuthServerUri([NotNullWhen(true)] out Uri? authServerUri)
+ => TryGetNormalizedAuthServerUri(AuthServerUrl, out authServerUri);
+
+ private static bool TryGetNormalizedAuthServerUri(string? url, [NotNullWhen(true)] out Uri? authServerUri)
+ {
+ authServerUri = null;
+ if (!Uri.TryCreate(url?.Trim(), UriKind.Absolute, out Uri? parsedUri)
+ || (!parsedUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
+ && !parsedUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
+ || string.IsNullOrWhiteSpace(parsedUri.Host)
+ || !string.IsNullOrEmpty(parsedUri.UserInfo)
+ || !string.IsNullOrEmpty(parsedUri.Query)
+ || !string.IsNullOrEmpty(parsedUri.Fragment))
+ {
+ return false;
+ }
+
+ var builder = new UriBuilder(parsedUri)
+ {
+ Path = parsedUri.AbsolutePath.TrimEnd('/') + "/"
+ };
+ authServerUri = builder.Uri;
+ return true;
+ }
public enum LoginType { mojang, microsoft, yggdrasil };
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md
index 008f22d3..4c9dc52d 100644
--- a/docs/guide/configuration.md
+++ b/docs/guide/configuration.md
@@ -138,6 +138,16 @@ Coordinate = { x = 145, y = 64, z = 2045 }
AccountType = "microsoft"
```
+#### Interactive login
+
+If both `Account.Login` and `Account.Password` are empty when MCC starts, it asks which account type to use instead of assuming Microsoft login.
+
+- Choose Offline to enter an in-game username. MCC saves the account as an offline account.
+- Choose Online (Microsoft) to continue with the device-code sign-in. MCC shows the code and link, then opens the sign-in page in your browser.
+- Choose Yggdrasil to enter the username, password, and authlib-injector URL for the account server. MCC checks the URL before it continues. If the address is malformed, unreachable, or does not return authlib-injector metadata, MCC explains the problem and asks for another URL.
+
+After a successful login, MCC saves the account details and selected account type in the configuration file. Later starts use those saved details and do not show this prompt. To choose a different method, edit or clear the account settings in the configuration file.
+
#### `Method`
- **Description:**
@@ -154,52 +164,32 @@ Coordinate = { x = 145, y = 64, z = 2045 }
Method = "mcc"
```
-#### `AuthServer`
+#### `AuthServerUrl`
- **Description:**
- This subsection is used when `AccountType` is set to `yggdrasil`. It points MCC at the authlib/Yggdrasil server used for login, session checks, and profile key requests.
+ Use this setting with `AccountType = "yggdrasil"`. It is the complete base URL of the authlib-injector or Yggdrasil service MCC uses for login, session checks, and profile key requests.
- MCC now writes this as a dedicated TOML subsection instead of an inline table:
+ The URL must start with `http://` or `https://` and include any path used by the service. For example, an authlib-injector server may use `/authlib-injector/` as its base path.
- ```toml
- [Main.General.AuthServer]
- ```
+ An existing multi-field Yggdrasil configuration is converted to this URL automatically. When MCC writes the configuration after migration, it removes the previous Yggdrasil section.
- `Host` accepts either a plain host name or a `host:port` pair. If you include the port there, MCC updates `Port` to match.
+- **Type:** `string`
- `AuthlibInjectorAPIPath` defaults to `/api/yggdrasil`. Change it if your authlib-injector server uses a different prefix, such as `/authlib-injector`.
-
- `UseHttps` defaults to `true`. Set it to `false` if your local or development auth server only exposes plain HTTP.
-
-- **Type:** `section`
-
-- **Default:**
-
- ```toml
- [Main.General.AuthServer]
- Port = 443
- AuthlibInjectorAPIPath = "/api/yggdrasil"
- UseHttps = true
- Host = ""
- ```
+- **Default:** `""`
- **Example:**
- ```
- [Main.General.AuthServer]
- Host = "auth.example.com"
- Port = 443
- AuthlibInjectorAPIPath = "/api/yggdrasil"
- UseHttps = true
+ ```toml
+ Account = { Login = "player@example.com", Password = "password" }
+ AccountType = "yggdrasil"
+ AuthServerUrl = "https://auth.example.com/api/yggdrasil/"
```
- ```
- [Main.General.AuthServer]
- Host = "127.0.0.1"
- Port = 25585
- AuthlibInjectorAPIPath = "/authlib-injector"
- UseHttps = false
+ ```toml
+ Account = { Login = "player", Password = "password" }
+ AccountType = "yggdrasil"
+ AuthServerUrl = "http://127.0.0.1:25585/authlib-injector/"
```
#### `AuthUser`