From 71eb1dca1750e9d490f39104bc44b91bd68eb1d8 Mon Sep 17 00:00:00 2001 From: ReinforceZwei <39955851+ReinforceZwei@users.noreply.github.com> Date: Sat, 6 Feb 2021 09:29:14 +0800 Subject: [PATCH] Implement browser sign-in method (#1447) * Implement browser sign-in method * Handle empty link * Improve * Handle user cancel login --- MinecraftClient/Program.cs | 11 +- .../Protocol/MicrosoftAuthentication.cs | 4 +- MinecraftClient/Protocol/ProtocolHandler.cs | 111 +++++++++++++++++- .../Resources/config/MinecraftClient.ini | 1 + MinecraftClient/Resources/lang/en.ini | 1 + MinecraftClient/Settings.cs | 4 + 6 files changed, 124 insertions(+), 8 deletions(-) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 9ef972f9..b831e5a5 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -136,16 +136,21 @@ namespace MinecraftClient } //Asking the user to type in missing data such as Username and Password - + bool useBrowser = Settings.AccountType == ProtocolHandler.AccountType.Microsoft && Settings.LoginMethod == "browser"; if (Settings.Login == "") { + if (useBrowser) + ConsoleIO.WriteLine("Press Enter to skip session cache checking and continue sign-in with browser"); Console.Write(ConsoleIO.BasicIO ? Translations.Get("mcc.login_basic_io") + "\n" : Translations.Get("mcc.login")); Settings.Login = Console.ReadLine(); } - if (Settings.Password == "" && (Settings.SessionCaching == CacheType.None || !SessionCache.Contains(Settings.Login.ToLower()))) + if (Settings.Password == "" + && (Settings.SessionCaching == CacheType.None || !SessionCache.Contains(Settings.Login.ToLower())) + && !useBrowser) { RequestPassword(); } + startupargs = args; InitializeClient(); @@ -209,7 +214,6 @@ namespace MinecraftClient SessionCache.Store(Settings.Login.ToLower(), session); } } - } if (result == ProtocolHandler.LoginResult.Success) @@ -315,6 +319,7 @@ namespace MinecraftClient case ProtocolHandler.LoginResult.NotPremium: failureReason = "error.login.premium"; break; case ProtocolHandler.LoginResult.OtherError: failureReason = "error.login.network"; break; case ProtocolHandler.LoginResult.SSLError: failureReason = "error.login.ssl"; break; + case ProtocolHandler.LoginResult.UserCancel: failureReason = "error.login.cancel"; break; default: failureReason = "error.login.unknown"; break; } failureMessage += Translations.Get(failureReason); diff --git a/MinecraftClient/Protocol/MicrosoftAuthentication.cs b/MinecraftClient/Protocol/MicrosoftAuthentication.cs index f84207b2..f110c5c5 100644 --- a/MinecraftClient/Protocol/MicrosoftAuthentication.cs +++ b/MinecraftClient/Protocol/MicrosoftAuthentication.cs @@ -21,6 +21,8 @@ namespace MinecraftClient.Protocol private Regex invalidAccount = new Regex("Sign in to", RegexOptions.IgnoreCase); private Regex twoFA = new Regex("Help us protect your account", RegexOptions.IgnoreCase); + public string SignInUrl { get { return authorize; } } + /// /// Pre-authentication /// @@ -114,7 +116,7 @@ namespace MinecraftClient.Protocol if (twoFA.IsMatch(response.Body)) { // TODO: Handle 2FA - throw new Exception("2FA enabled but not supported yet. Try to disable it in Microsoft account settings"); + throw new Exception("2FA enabled but not supported yet. Use browser sign-in method or try to disable 2FA in Microsoft account settings"); } else if (invalidAccount.IsMatch(response.Body)) { diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index ca5bae7d..d0a698c7 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -330,7 +330,7 @@ namespace MinecraftClient.Protocol return Protocol18Forge.ServerForceForge(protocol); } - public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, InvalidResponse, NullError }; + public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, InvalidResponse, NullError, UserCancel }; public enum AccountType { Mojang, Microsoft }; /// @@ -344,7 +344,10 @@ namespace MinecraftClient.Protocol { if (type == AccountType.Microsoft) { - return MicrosoftLogin(user, pass, out session); + if (Settings.LoginMethod == "mcc") + return MicrosoftMCCLogin(user, pass, out session); + else + return MicrosoftBrowserLogin(out session); } else if (type == AccountType.Mojang) { @@ -353,6 +356,13 @@ namespace MinecraftClient.Protocol else throw new InvalidOperationException("Account type must be Mojang or Microsoft"); } + /// + /// Login using Mojang account. Will be outdated after account migration + /// + /// + /// + /// + /// private static LoginResult MojangLogin(string user, string pass, out SessionToken session) { session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; @@ -432,7 +442,101 @@ namespace MinecraftClient.Protocol } } - private static LoginResult MicrosoftLogin(string email, string password, out SessionToken session) + /// + /// Sign-in to Microsoft Account without using browser. Only works if 2FA is disabled. + /// Might not work well in some rare cases. + /// + /// + /// + /// + /// + private static LoginResult MicrosoftMCCLogin(string email, string password, out SessionToken session) + { + var ms = new XboxLive(); + try + { + var msaResponse = ms.UserLogin(email, password, ms.PreAuth()); + return MicrosoftLogin(msaResponse, out session); + } + catch (Exception e) + { + session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); + if (Settings.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + return LoginResult.WrongPassword; // Might not always be wrong password + } + } + + /// + /// Sign-in to Microsoft Account by asking user to open sign-in page using browser. + /// + /// + /// The downside is this require user to copy and paste lengthy content from and to console. + /// Sign-in page: 218 chars + /// Response URL: around 1500 chars + /// + /// + /// + public static LoginResult MicrosoftBrowserLogin(out SessionToken session) + { + var ms = new XboxLive(); + string[] askOpenLink = + { + "Copy the following link to your browser and login to your Microsoft Account", + ">>>>>>>>>>>>>>>>>>>>>>", + "", + ms.SignInUrl, + "", + "<<<<<<<<<<<<<<<<<<<<<<", + "NOTICE: Once successfully logged in, you will see a blank page in your web browser.", + "Copy the contents of your browser's address bar and paste it below to complete the login process.", + }; + ConsoleIO.WriteLine(string.Join("\n", askOpenLink)); + string[] parts = { }; + while (true) + { + string link = ConsoleIO.ReadLine(); + if (string.IsNullOrEmpty(link)) + { + session = new SessionToken(); + return LoginResult.UserCancel; + } + parts = link.Split('#'); + if (parts.Length < 2) + { + ConsoleIO.WriteLine("Invalid link. Please try again."); + continue; + } + else break; + } + string hash = parts[1]; + var dict = Request.ParseQueryString(hash); + var msaResponse = new XboxLive.UserLoginResponse() + { + AccessToken = dict["access_token"], + RefreshToken = dict["refresh_token"], + ExpiresIn = int.Parse(dict["expires_in"]) + }; + try + { + return MicrosoftLogin(msaResponse, out session); + } + catch (Exception e) + { + session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); + if (Settings.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + return LoginResult.WrongPassword; // Might not always be wrong password + } + } + + private static LoginResult MicrosoftLogin(XboxLive.UserLoginResponse msaResponse, out SessionToken session) { session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; var ms = new XboxLive(); @@ -440,7 +544,6 @@ namespace MinecraftClient.Protocol try { - var msaResponse = ms.UserLogin(email, password, ms.PreAuth()); var xblResponse = ms.XblAuthenticate(msaResponse); var xsts = ms.XSTSAuthenticate(xblResponse); // Might throw even password correct diff --git a/MinecraftClient/Resources/config/MinecraftClient.ini b/MinecraftClient/Resources/config/MinecraftClient.ini index 7e5a071d..5573a4f8 100644 --- a/MinecraftClient/Resources/config/MinecraftClient.ini +++ b/MinecraftClient/Resources/config/MinecraftClient.ini @@ -10,6 +10,7 @@ login= password= serverip= type=mojang # Account type. mojang or microsoft +method=mcc # Microsoft Account sign-in method. mcc OR browser # Advanced settings diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 6a533201..8da998ac 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -66,6 +66,7 @@ error.login.network=Network error. error.login.ssl=SSL Error. error.login.unknown=Unknown Error. error.login.ssl_help=§8It appears that you are using Mono to run this program.\nThe first time, you have to import HTTPS certificates using:\nmozroots --import --ask-remove +error.login.cancel=User cancelled. error.login_failed=Failed to login to this server. error.join=Failed to join this server. error.connect=Failed to connect to this IP. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 5cd0c695..79a92657 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -26,6 +26,7 @@ namespace MinecraftClient public static string Username = ""; public static string Password = ""; public static ProtocolHandler.AccountType AccountType = ProtocolHandler.AccountType.Mojang; + public static string LoginMethod = "mcc"; public static string ServerIP = ""; public static ushort ServerPort = 25565; public static string ServerVersion = ""; @@ -277,6 +278,9 @@ namespace MinecraftClient case "type": AccountType = argValue == "mojang" ? ProtocolHandler.AccountType.Mojang : ProtocolHandler.AccountType.Microsoft; break; + case "method": LoginMethod = argValue.ToLower() == "browser" + ? "browser" + : "mcc"; break; case "serverip": if (!SetServerIP(argValue)) serverAlias = argValue; ; break; case "singlecommand": SingleCommand = argValue; break; case "language": Language = argValue; break;