diff --git a/.gitignore b/.gitignore index 10a7f2a6..db5e7e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ /.vs/ SessionCache.ini .* -!/.github \ No newline at end of file +!/.github +/packages \ No newline at end of file diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 1269a4f6..187f6d02 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -1,427 +1,428 @@ - - - - Debug - x86 - 8.0.30703 - 2.0 - {1E2FACE4-F5CA-4323-9641-740C6A551770} - Exe - Properties - MinecraftClient - MinecraftClient - v4.0 - Client - 512 - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - false - - - Resources\AppIcon.ico - - - MinecraftClient.Program - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - DefaultConfigResource.resx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - Microsoft .NET Framework 4 Client Profile %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - false - - - False - Windows Installer 3.1 - true - - - - - - - - - - - - - - - PublicResXFileCodeGenerator - DefaultConfigResource.Designer.cs - - - - - - - - - + + + + Debug + x86 + 8.0.30703 + 2.0 + {1E2FACE4-F5CA-4323-9641-740C6A551770} + Exe + Properties + MinecraftClient + MinecraftClient + v4.0 + Client + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + false + + + Resources\AppIcon.ico + + + MinecraftClient.Program + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + DefaultConfigResource.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + Microsoft .NET Framework 4 Client Profile %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + + + + + + + + + + + PublicResXFileCodeGenerator + DefaultConfigResource.Designer.cs + + + + + + + + + \ No newline at end of file diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index ba56f824..2451ac53 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -143,10 +143,8 @@ 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 (Settings.Login == "" && !useBrowser) { - 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(); } @@ -213,7 +211,7 @@ namespace MinecraftClient if (result != ProtocolHandler.LoginResult.Success) { Translations.WriteLineFormatted("mcc.session_invalid"); - if (Settings.Password == "") + if (Settings.Password == "" && Settings.AccountType == ProtocolHandler.AccountType.Mojang) RequestPassword(); } else ConsoleIO.WriteLineFormatted(Translations.Get("mcc.session_valid", session.PlayerName)); diff --git a/MinecraftClient/Protocol/JwtPayloadDecode.cs b/MinecraftClient/Protocol/JwtPayloadDecode.cs new file mode 100644 index 00000000..b3626ee8 --- /dev/null +++ b/MinecraftClient/Protocol/JwtPayloadDecode.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient.Protocol +{ + // Thanks to https://stackoverflow.com/questions/60404612/parse-jwt-token-to-get-the-payload-content-only-without-external-library-in-c-sh + public static class JwtPayloadDecode + { + public static string GetPayload(string token) + { + var content = token.Split('.')[1]; + var jsonPayload = Encoding.UTF8.GetString(Decode(content)); + return jsonPayload; + } + + private static byte[] Decode(string input) + { + var output = input; + output = output.Replace('-', '+'); // 62nd char of encoding + output = output.Replace('_', '/'); // 63rd char of encoding + switch (output.Length % 4) // Pad with trailing '='s + { + case 0: break; // No pad chars in this case + case 2: output += "=="; break; // Two pad chars + case 3: output += "="; break; // One pad char + default: throw new System.ArgumentOutOfRangeException("input", "Illegal base64url string!"); + } + var converted = Convert.FromBase64String(output); // Standard base64 decoder + return converted; + } + } +} diff --git a/MinecraftClient/Protocol/MicrosoftAuthentication.cs b/MinecraftClient/Protocol/MicrosoftAuthentication.cs index f110c5c5..4164f3e2 100644 --- a/MinecraftClient/Protocol/MicrosoftAuthentication.cs +++ b/MinecraftClient/Protocol/MicrosoftAuthentication.cs @@ -4,9 +4,110 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Collections.Specialized; +using System.Diagnostics; namespace MinecraftClient.Protocol { + static class Microsoft + { + private static readonly string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3"; + private static readonly string signinUrl = string.Format("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={0}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment", clientId); + private static readonly string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + public static string SignInUrl { get { return signinUrl; } } + + /// + /// Get a sign-in URL with email field pre-filled + /// + /// Login Email + /// Sign-in URL with email pre-filled + public static string GetSignInUrlWithHint(string loginHint) + { + return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint); + } + + /// + /// Request access token by auth code + /// + /// Auth code obtained after user signing in + /// Access token and refresh token + public static LoginResponse RequestAccessToken(string code) + { + string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}"; + postData = string.Format(postData, clientId, code); + return RequestToken(postData); + } + + /// + /// Request access token by refresh token + /// + /// Refresh token + /// Access token and new refresh token + public static LoginResponse RefreshAccessToken(string refreshToken) + { + string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}"; + postData = string.Format(postData, clientId, refreshToken); + return RequestToken(postData); + } + + /// + /// Perform request to obtain access token by code or by refresh token + /// + /// Complete POST data for the request + /// + private static LoginResponse RequestToken(string postData) + { + var request = new ProxiedWebRequest(tokenUrl); + request.UserAgent = "MCC/" + Program.Version; + var response = request.Post("application/x-www-form-urlencoded", postData); + var jsonData = Json.ParseJson(response.Body); + + // Error handling + if (jsonData.Properties.ContainsKey("error")) + { + throw new Exception(jsonData.Properties["error_description"].StringValue); + } + else + { + string accessToken = jsonData.Properties["access_token"].StringValue; + string refreshToken = jsonData.Properties["refresh_token"].StringValue; + int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue); + + // Extract email from JWT + string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue); + var jsonPayload = Json.ParseJson(payload); + string email = jsonPayload.Properties["email"].StringValue; + return new LoginResponse() + { + Email = email, + AccessToken = accessToken, + RefreshToken = refreshToken, + ExpiresIn = expiresIn + }; + } + } + + public static void OpenBrowser(string link) + { + try + { + Process.Start(link); + } + catch (Exception e) + { + ConsoleIO.WriteLine("Cannot open browser\n" + e.Message + "\n" + e.StackTrace); + } + } + + public struct LoginResponse + { + public string Email; + public string AccessToken; + public string RefreshToken; + public int ExpiresIn; + } + } + class XboxLive { private readonly string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en"; @@ -63,7 +164,7 @@ namespace MinecraftClient.Protocol /// Account password /// /// - public UserLoginResponse UserLogin(string email, string password, PreAuthResponse preAuth) + public Microsoft.LoginResponse UserLogin(string email, string password, PreAuthResponse preAuth) { var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie); request.UserAgent = userAgent; @@ -104,8 +205,9 @@ namespace MinecraftClient.Protocol // Console.WriteLine("{0}: {1}", pair.Key, pair.Value); //} - return new UserLoginResponse() + return new Microsoft.LoginResponse() { + Email = email, AccessToken = dict["access_token"], RefreshToken = dict["refresh_token"], ExpiresIn = int.Parse(dict["expires_in"]) @@ -131,18 +233,26 @@ namespace MinecraftClient.Protocol /// /// /// - public XblAuthenticateResponse XblAuthenticate(UserLoginResponse loginResponse) + public XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse) { var request = new ProxiedWebRequest(xbl); request.UserAgent = userAgent; request.Accept = "application/json"; request.Headers.Add("x-xbl-contract-version", "0"); + var accessToken = loginResponse.AccessToken; + if (Settings.LoginMethod == "browser") + { + // Our own client ID must have d= in front of the token or HTTP status 400 + // "Stolen" client ID must not have d= in front of the token or HTTP status 400 + accessToken = "d=" + accessToken; + } + string payload = "{" + "\"Properties\": {" + "\"AuthMethod\": \"RPS\"," + "\"SiteName\": \"user.auth.xboxlive.com\"," - + "\"RpsTicket\": \"" + loginResponse.AccessToken + "\"" + + "\"RpsTicket\": \"" + accessToken + "\"" + "}," + "\"RelyingParty\": \"http://auth.xboxlive.com\"," + "\"TokenType\": \"JWT\"" @@ -241,13 +351,6 @@ namespace MinecraftClient.Protocol public NameValueCollection Cookie; } - public struct UserLoginResponse - { - public string AccessToken; - public string RefreshToken; - public int ExpiresIn; - } - public struct XblAuthenticateResponse { public string Token; diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 098445bb..572832ec 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -357,7 +357,7 @@ namespace MinecraftClient.Protocol if (Settings.LoginMethod == "mcc") return MicrosoftMCCLogin(user, pass, out session); else - return MicrosoftBrowserLogin(out session); + return MicrosoftBrowserLogin(out session, user); } else if (type == AccountType.Mojang) { @@ -490,46 +490,19 @@ namespace MinecraftClient.Protocol /// /// /// - public static LoginResult MicrosoftBrowserLogin(out SessionToken session) + public static LoginResult MicrosoftBrowserLogin(out SessionToken session, string loginHint = "") { - 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"]) - }; + if (string.IsNullOrEmpty(loginHint)) + Microsoft.OpenBrowser(Microsoft.SignInUrl); + else + Microsoft.OpenBrowser(Microsoft.GetSignInUrlWithHint(loginHint)); + ConsoleIO.WriteLine("Your browser should open automatically. If not, open the link below in your browser."); + ConsoleIO.WriteLine("\n" + Microsoft.SignInUrl + "\n"); + + ConsoleIO.WriteLine("Paste your code here"); + string code = ConsoleIO.ReadLine(); + + var msaResponse = Microsoft.RequestAccessToken(code); try { return MicrosoftLogin(msaResponse, out session); @@ -546,7 +519,7 @@ namespace MinecraftClient.Protocol } } - private static LoginResult MicrosoftLogin(XboxLive.UserLoginResponse msaResponse, out SessionToken session) + private static LoginResult MicrosoftLogin(Microsoft.LoginResponse msaResponse, out SessionToken session) { session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; var ms = new XboxLive(); @@ -565,6 +538,7 @@ namespace MinecraftClient.Protocol session.PlayerName = profile.UserName; session.PlayerID = profile.UUID; session.ID = accessToken; + Settings.Login = msaResponse.Email; return LoginResult.Success; } else @@ -590,27 +564,24 @@ namespace MinecraftClient.Protocol /// Returns the status of the token (Valid, Invalid, etc.) public static LoginResult GetTokenValidation(SessionToken session) { - try + var payload = JwtPayloadDecode.GetPayload(session.ID); + var json = Json.ParseJson(payload); + var expTimestamp = long.Parse(json.Properties["exp"].StringValue); + var now = DateTime.Now; + var tokenExp = UnixTimeStampToDateTime(expTimestamp); + if (Settings.DebugMessages) { - string result = ""; - string json_request = "{\"accessToken\": \"" + JsonEncode(session.ID) + "\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }"; - int code = DoHTTPSPost("authserver.mojang.com", "/validate", json_request, ref result); - if (code == 204) - { - return LoginResult.Success; - } - else if (code == 403) - { - return LoginResult.LoginRequired; - } - else - { - return LoginResult.OtherError; - } + ConsoleIO.WriteLine("Access token expiration time is " + tokenExp.ToString()); } - catch + if (now < tokenExp) { - return LoginResult.OtherError; + // Still valid + return LoginResult.Success; + } + else + { + // Token expired + return LoginResult.LoginRequired; } } @@ -897,5 +868,18 @@ namespace MinecraftClient.Protocol return result.ToString(); } + + /// + /// Convert a TimeStamp (in second) to DateTime object + /// + /// TimeStamp in second + /// DateTime object of the TimeStamp + public static DateTime UnixTimeStampToDateTime(long unixTimeStamp) + { + // Unix timestamp is seconds past epoch + DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime(); + return dateTime; + } } }