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;