diff --git a/app/src/main/java/com/kdt/mojangauth/RefreshListener.java b/app/src/main/java/com/kdt/mojangauth/RefreshListener.java index 46c3efc1a..c5dd225c9 100644 --- a/app/src/main/java/com/kdt/mojangauth/RefreshListener.java +++ b/app/src/main/java/com/kdt/mojangauth/RefreshListener.java @@ -1,7 +1,9 @@ package com.kdt.mojangauth; +import net.kdt.pojavlaunch.*; + public interface RefreshListener { public void onFailed(Throwable e); - public void onSuccess(); + public void onSuccess(MCProfile.Builder profile); } diff --git a/app/src/main/java/com/kdt/mojangauth/RefreshTokenTask.java b/app/src/main/java/com/kdt/mojangauth/RefreshTokenTask.java index 37bee4d06..0c608a6e6 100644 --- a/app/src/main/java/com/kdt/mojangauth/RefreshTokenTask.java +++ b/app/src/main/java/com/kdt/mojangauth/RefreshTokenTask.java @@ -65,7 +65,7 @@ public class RefreshTokenTask extends AsyncTask { public void onPostExecute(Throwable result) { build.dismiss(); if (result == null) { - listener.onSuccess(); + listener.onSuccess(null); } else { listener.onFailed(result); } diff --git a/app/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java b/app/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java index d74275d07..184585da5 100644 --- a/app/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java +++ b/app/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java @@ -327,8 +327,9 @@ public class PojavLoginActivity extends BaseActivity } @Override - public void onSuccess() { - // TODO: Implement this method + public void onSuccess(MCProfile.Builder b) { + mProfile = b; + playProfile(); } }).execute(code); // Toast.makeText(this, "Logged in to Microsoft account, but NYI", Toast.LENGTH_LONG).show(); @@ -733,7 +734,7 @@ public class PojavLoginActivity extends BaseActivity } @Override - public void onSuccess() + public void onSuccess(MCProfile.Builder unused) { MCProfile.launch(PojavLoginActivity.this, path); } diff --git a/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/HttpResponse.java b/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/HttpResponse.java index 72fd0c749..9f5695dc5 100644 --- a/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/HttpResponse.java +++ b/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/HttpResponse.java @@ -27,7 +27,11 @@ public class HttpResponse public T body() throws IOException { Class type = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; if (type.isAssignableFrom(String.class)) { - return (T) Tools.read(mRequest.mBuilder.getBase().getInputStream()); + if (statusCode() >= 200 && statusCode() < 300) { + return (T) Tools.read(mRequest.mBuilder.getBase().getInputStream()); + } else { + return (T) Tools.read(mRequest.mBuilder.getBase().getErrorStream()); + } } else { throw new UnsupportedOperationException(); } diff --git a/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthenticator.java b/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthenticator.java index 4acedf438..adbdf81a8 100644 --- a/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthenticator.java +++ b/app/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthenticator.java @@ -15,13 +15,15 @@ import org.json.*; import java.text.ParseException; import java.io.*; +import net.kdt.pojavlaunch.value.launcherprofiles.*; +import net.kdt.pojavlaunch.value.*; -public class MicrosoftAuthenticator extends AsyncTask { +public class MicrosoftAuthenticator extends AsyncTask { private static final String authTokenUrl = "https://login.live.com/oauth20_token.srf"; private static final String xblAuthUrl = "https://user.auth.xboxlive.com/user/authenticate"; private static final String xstsAuthUrl = "https://xsts.auth.xboxlive.com/xsts/authorize"; private static final String mcLoginUrl = "https://api.minecraftservices.com/authentication/login_with_xbox"; - private static final String mcStoreUrl = "https://api.minecraftservices.com/entitlements/mcstore"; + // private static final String mcStoreUrl = "https://api.minecraftservices.com/entitlements/mcstore"; private static final String mcProfileUrl = "https://api.minecraftservices.com/minecraft/profile"; //private Gson gson = new Gson(); @@ -45,9 +47,8 @@ public class MicrosoftAuthenticator extends AsyncTask { } @Override - public Throwable doInBackground(String... args) { + public Object doInBackground(String... args) { try { - MCProfile.Builder profilePath = MCProfile.load(args[0]); String authCode = args[1]; publishProgress(); @@ -57,16 +58,24 @@ public class MicrosoftAuthenticator extends AsyncTask { String xblToken = acquireXBLToken(msaAccessToken); publishProgress(); - // TODO + String[] xstsData = acquireXsts(xblToken); - /* - profilePath.setClientID(response.clientToken.toString()); - profilePath.setAccessToken(response.accessToken); - profilePath.setUsername(response.selectedProfile.name); - profilePath.setProfileID(response.selectedProfile.id); + publishProgress(); + String mcAccessToken = acquireMinecraftToken(xstsData[0], xstsData[1]); + + publishProgress(); + // TODO migrate account format to json + MinecraftAccount acc = checkMcProfile(mcAccessToken); + + MCProfile.Builder profilePath = MCProfile.load(args[0]); + + profilePath.setClientID("0" /* FIXME */); + profilePath.setAccessToken(acc.accessToken); + profilePath.setUsername(acc.username); + profilePath.setProfileID(acc.profileId); MCProfile.build(profilePath); - */ - return null; + + return profilePath; } catch (Throwable e) { return e; } @@ -76,16 +85,15 @@ public class MicrosoftAuthenticator extends AsyncTask { protected void onProgressUpdate(Void[] p1) { super.onProgressUpdate(p1); build.setProgress(build.getProgress() + 1); - } @Override - public void onPostExecute(Throwable result) { + public void onPostExecute(Object result) { build.dismiss(); - if (result == null) { - listener.onSuccess(); + if (result instanceof MCProfile.Builder) { + listener.onSuccess((MCProfile.Builder) result); } else { - listener.onFailed(result); + listener.onFailed((Throwable) result); } } @@ -115,12 +123,27 @@ public class MicrosoftAuthenticator extends AsyncTask { OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + private final class XSTSXUI { + private String uhs; + } + + private final class XSTSDisplayClaims { + private XSTSXUI[] xui; + } + private final class GlobalToken { // MSA AccessToken private String access_token; - // XBL Token + // XBL, XSTS Token private String Token; + + // XSTS + private XSTSDisplayClaims DisplayClaims; + + // Minecraft side + private String id; + private String name; } private String acquireAccessToken(String authcode) throws IOException, URISyntaxException{ @@ -137,8 +160,12 @@ public class MicrosoftAuthenticator extends AsyncTask { .header("Content-Type", "application/x-www-form-urlencoded") .POST(ofFormData(data)).build(); - HttpResponse response = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - return Tools.GLOBAL_GSON.fromJson((String) response.body(), GlobalToken.class).access_token; + HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() >= 200 && resp.statusCode() < 300) { + return Tools.GLOBAL_GSON.fromJson((String) resp.body(), GlobalToken.class).access_token; + } else { + throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); + } } private String acquireXBLToken(String accessToken) throws IOException, URISyntaxException { @@ -160,7 +187,89 @@ public class MicrosoftAuthenticator extends AsyncTask { .POST(ofJSONData(data)).build(); HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - return Tools.GLOBAL_GSON.fromJson((String) resp.body(), GlobalToken.class).Token; + if (resp.statusCode() >= 200 && resp.statusCode() < 300) { + return Tools.GLOBAL_GSON.fromJson((String) resp.body(), GlobalToken.class).Token; + } else { + throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); + } + } + + private String[] acquireXsts(String xblToken) throws IOException, URISyntaxException { + URI uri = new URI(xstsAuthUrl); + + Map dataProp = new ArrayMap<>(); + dataProp.put("SandboxId", "RETAIL"); + dataProp.put("UserTokens", Arrays.asList(xblToken)); + + Map data = new ArrayMap<>(); + data.put("Properties", dataProp); + data.put("RelyingParty", "rp://api.minecraftservices.com/"); + data.put("TokenType", "JWT"); + + HttpRequest request = HttpRequest.newBuilder(uri) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .POST(ofJSONData(data)).build(); + + HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); + + if (resp.statusCode() >= 200 && resp.statusCode() < 300) { + String body = (String) resp.body(); + GlobalToken token = Tools.GLOBAL_GSON.fromJson(body, GlobalToken.class); + String xblXsts = token.Token; + String uhs = token.DisplayClaims.xui[0].uhs; + + return new String[]{uhs, xblXsts}; + } else { + throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); + } + } + + private String acquireMinecraftToken(String xblUhs, String xblXsts) throws IOException, URISyntaxException { + URI uri = new URI(mcLoginUrl); + + Map data = new ArrayMap<>(); + data.put("identityToken", "XBL3.0 x=" + xblUhs + ";" + xblXsts); + + HttpRequest request = HttpRequest.newBuilder(uri) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .POST(ofJSONData(data)).build(); + + HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() >= 200 && resp.statusCode() < 300) { + String body = (String) resp.body(); + return Tools.GLOBAL_GSON.fromJson(body, GlobalToken.class).access_token; + } else { + throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); + } + } + + private MinecraftAccount checkMcProfile(String mcAccessToken) throws IOException, URISyntaxException { + URI uri = new URI(mcProfileUrl); + + HttpRequest request = HttpRequest.newBuilder(uri) + .header("Authorization", "Bearer " + mcAccessToken) + .GET().build(); + + HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() >= 200 && resp.statusCode() < 300) { + String body = (String) resp.body(); + GlobalToken token = Tools.GLOBAL_GSON.fromJson(body, GlobalToken.class); + String uuid = token.id; + String uuidDashes = uuid.replaceFirst( + "(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5" + ); + + MinecraftAccount acc = new MinecraftAccount(); + acc.isMicrosoft = true; + acc.username = token.name; + acc.accessToken = mcAccessToken; + acc.profileId = uuidDashes; + return acc; + } else { + throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); + } } public static HttpRequest.BodyPublisher ofJSONData(Map data) { diff --git a/app/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java b/app/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java new file mode 100644 index 000000000..13ee24f1a --- /dev/null +++ b/app/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java @@ -0,0 +1,11 @@ +package net.kdt.pojavlaunch.value; + +public class MinecraftAccount +{ + public String accessToken; // access token + public String clientToken; // clientID: refresh and invalidate + public String profileId; // authenticate UUID + public String username; + public String selectedVersion = "1.7.10"; + public boolean isMicrosoft; +}