using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Specialized;
namespace MinecraftClient.Protocol
{
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";
private readonly string xbl = "https://user.auth.xboxlive.com/user/authenticate";
private readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
private readonly string userAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
private Regex ppft = new Regex("sFTTag:'.*value=\"(.*)\"\\/>'");
private Regex urlPost = new Regex("urlPost:'(.+?(?=\'))");
private Regex confirm = new Regex("identity\\/confirm");
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
///
/// This step is to get the login page for later use
///
public PreAuthResponse PreAuth()
{
var request = new ProxiedWebRequest(authorize);
request.UserAgent = userAgent;
var response = request.Get();
string html = response.Body;
string PPFT = ppft.Match(html).Groups[1].Value;
string urlPost = this.urlPost.Match(html).Groups[1].Value;
if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost))
{
throw new Exception("Fail to extract PPFT or urlPost");
}
//Console.WriteLine("PPFT: {0}", PPFT);
//Console.WriteLine();
//Console.WriteLine("urlPost: {0}", urlPost);
return new PreAuthResponse()
{
UrlPost = urlPost,
PPFT = PPFT,
Cookie = response.Cookies
};
}
///
/// Perform login request
///
/// This step is to send the login request by using the PreAuth response
/// Microsoft account email
/// Account password
///
///
public UserLoginResponse UserLogin(string email, string password, PreAuthResponse preAuth)
{
var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie);
request.UserAgent = userAgent;
string postData = "login=" + Uri.EscapeDataString(email)
+ "&loginfmt=" + Uri.EscapeDataString(email)
+ "&passwd=" + Uri.EscapeDataString(password)
+ "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT);
var response = request.Post("application/x-www-form-urlencoded", postData);
if (Settings.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode >= 300 && response.StatusCode <= 399)
{
string url = response.Headers.Get("Location");
string hash = url.Split('#')[1];
var request2 = new ProxiedWebRequest(url);
var response2 = request2.Get();
if (response2.StatusCode != 200)
{
throw new Exception("Authentication failed");
}
if (string.IsNullOrEmpty(hash))
{
throw new Exception("Cannot extract access token");
}
var dict = Request.ParseQueryString(hash);
//foreach (var pair in dict)
//{
// Console.WriteLine("{0}: {1}", pair.Key, pair.Value);
//}
return new UserLoginResponse()
{
AccessToken = dict["access_token"],
RefreshToken = dict["refresh_token"],
ExpiresIn = int.Parse(dict["expires_in"])
};
}
else
{
if (twoFA.IsMatch(response.Body))
{
// TODO: Handle 2FA
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))
{
throw new Exception("Invalid credentials. Check your credentials");
}
else throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
}
}
///
/// Xbox Live Authenticate
///
///
///
public XblAuthenticateResponse XblAuthenticate(UserLoginResponse loginResponse)
{
var request = new ProxiedWebRequest(xbl);
request.UserAgent = userAgent;
request.Accept = "application/json";
request.Headers.Add("x-xbl-contract-version", "0");
string payload = "{"
+ "\"Properties\": {"
+ "\"AuthMethod\": \"RPS\","
+ "\"SiteName\": \"user.auth.xboxlive.com\","
+ "\"RpsTicket\": \"" + loginResponse.AccessToken + "\""
+ "},"
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
{
string jsonString = response.Body;
//Console.WriteLine(jsonString);
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
return new XblAuthenticateResponse()
{
Token = token,
UserHash = userHash
};
}
else
{
throw new Exception("XBL Authentication failed");
}
}
///
/// XSTS Authenticate
///
/// (Don't ask me what is XSTS, I DONT KNOW)
///
///
public XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
{
var request = new ProxiedWebRequest(xsts);
request.UserAgent = userAgent;
request.Accept = "application/json";
request.Headers.Add("x-xbl-contract-version", "1");
string payload = "{"
+ "\"Properties\": {"
+ "\"SandboxId\": \"RETAIL\","
+ "\"UserTokens\": ["
+ "\"" + xblResponse.Token + "\""
+ "]"
+ "},"
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
{
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
return new XSTSAuthenticateResponse()
{
Token = token,
UserHash = userHash
};
}
else
{
if (response.StatusCode == 401)
{
Json.JSONData json = Json.ParseJson(response.Body);
if (json.Properties["XErr"].StringValue == "2148916233")
{
throw new Exception("The account doesn't have an Xbox account");
}
else if (json.Properties["XErr"].StringValue == "2148916238")
{
throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult");
}
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue);
}
else
{
throw new Exception("XSTS Authentication failed");
}
}
}
public struct PreAuthResponse
{
public string UrlPost;
public string PPFT;
public NameValueCollection Cookie;
}
public struct UserLoginResponse
{
public string AccessToken;
public string RefreshToken;
public int ExpiresIn;
}
public struct XblAuthenticateResponse
{
public string Token;
public string UserHash;
}
public struct XSTSAuthenticateResponse
{
public string Token;
public string UserHash;
}
}
class MinecraftWithXbox
{
private readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
private readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
private readonly string profile = "https://api.minecraftservices.com/minecraft/profile";
///
/// Login to Minecraft using the XSTS token and user hash obtained before
///
///
///
///
public string LoginWithXbox(string userHash, string xstsToken)
{
var request = new ProxiedWebRequest(loginWithXbox);
request.Accept = "application/json";
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
var response = request.Post("application/json", payload);
if (Settings.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["access_token"].StringValue;
}
///
/// Check if user own Minecraft by access token
///
///
/// True if the user own the game
public bool UserHasGame(string accessToken)
{
var request = new ProxiedWebRequest(ownership);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
if (Settings.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["items"].DataArray.Count > 0;
}
public UserProfile GetUserProfile(string accessToken)
{
var request = new ProxiedWebRequest(profile);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
if (Settings.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return new UserProfile()
{
UUID = json.Properties["id"].StringValue,
UserName = json.Properties["name"].StringValue
};
}
public struct UserProfile
{
public string UUID;
public string UserName;
}
}
///
/// Helper class
///
static class Request
{
static public Dictionary ParseQueryString(string query)
{
return query.Split('&')
.ToDictionary(c => c.Split('=')[0],
c => Uri.UnescapeDataString(c.Split('=')[1]));
}
}
}