Microsoft Sign-in: Migrate to our own client Id (#1827)

* Microsoft Sign-in: Migrate to our own client Id

- Drop support of "mcc" sign-in method
- Add nuget packages for decoding JWT

* Remove JWT nuget package

* Remove client secret

It is not needed after changing application type in Azure

* Change token validation method to expiration time

* Revert changes of dropping mcc sign-in method

* Add email pre-fill for browser sign-in
This commit is contained in:
ReinforceZwei 2021-12-04 19:15:58 +08:00 committed by GitHub
parent 9a9245f193
commit fdc3069083
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 622 additions and 501 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@
SessionCache.ini
.*
!/.github
/packages

View file

@ -75,6 +75,7 @@
<Compile Include="AutoTimeout.cs" />
<Compile Include="ChatBots\AutoDrop.cs" />
<Compile Include="ChatBots\Mailer.cs" />
<Compile Include="Protocol\JwtPayloadDecode.cs" />
<Compile Include="Protocol\Handlers\PacketPalettes\PacketPalette118.cs" />
<Compile Include="Protocol\MojangAPI.cs" />
<Compile Include="Mapping\CubeFromWorld.cs" />

View file

@ -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));

View file

@ -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;
}
}
}

View file

@ -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; } }
/// <summary>
/// Get a sign-in URL with email field pre-filled
/// </summary>
/// <param name="loginHint">Login Email</param>
/// <returns>Sign-in URL with email pre-filled</returns>
public static string GetSignInUrlWithHint(string loginHint)
{
return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint);
}
/// <summary>
/// Request access token by auth code
/// </summary>
/// <param name="code">Auth code obtained after user signing in</param>
/// <returns>Access token and refresh token</returns>
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);
}
/// <summary>
/// Request access token by refresh token
/// </summary>
/// <param name="refreshToken">Refresh token</param>
/// <returns>Access token and new refresh token</returns>
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);
}
/// <summary>
/// Perform request to obtain access token by code or by refresh token
/// </summary>
/// <param name="postData">Complete POST data for the request</param>
/// <returns></returns>
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
/// <param name="password">Account password</param>
/// <param name="preAuth"></param>
/// <returns></returns>
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
/// </summary>
/// <param name="loginResponse"></param>
/// <returns></returns>
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;

View file

@ -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
/// </remarks>
/// <param name="session"></param>
/// <returns></returns>
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>Returns the status of the token (Valid, Invalid, etc.)</returns>
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;
ConsoleIO.WriteLine("Access token expiration time is " + tokenExp.ToString());
}
else if (code == 403)
if (now < tokenExp)
{
return LoginResult.LoginRequired;
// Still valid
return LoginResult.Success;
}
else
{
return LoginResult.OtherError;
}
}
catch
{
return LoginResult.OtherError;
// Token expired
return LoginResult.LoginRequired;
}
}
@ -897,5 +868,18 @@ namespace MinecraftClient.Protocol
return result.ToString();
}
/// <summary>
/// Convert a TimeStamp (in second) to DateTime object
/// </summary>
/// <param name="unixTimeStamp">TimeStamp in second</param>
/// <returns>DateTime object of the TimeStamp</returns>
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;
}
}
}