From c11eae582fe134e2cf7aafb1680ec85f5281b8d9 Mon Sep 17 00:00:00 2001 From: tomikun <60690056+alexytomi@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:55:57 +0800 Subject: [PATCH] fix(MicrosoftBackgroundLogin): Add proper detection for ownership It only detected profile before, now it checks for real ownership of the game and gives out the proper error --- .../microsoft/MicrosoftBackgroundLogin.java | 141 +++++++++++++----- .../src/main/res/values/strings.xml | 1 + 2 files changed, 105 insertions(+), 37 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java index 351c6e268..2667a5bae 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java @@ -3,6 +3,7 @@ package net.kdt.pojavlaunch.authenticator.microsoft; import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; import android.util.ArrayMap; +import android.util.Base64; import android.util.Log; import androidx.annotation.NonNull; @@ -59,7 +60,7 @@ public class MicrosoftBackgroundLogin { public String mcName; public String mcToken; public String mcUuid; - public boolean doesOwnGame; + public boolean doesOwnGame, hasProfile = false; public long expiresAt; public MicrosoftBackgroundLogin(boolean isRefresh, String authCode){ @@ -82,8 +83,16 @@ public class MicrosoftBackgroundLogin { notifyProgress(progressListener, 4); String mcToken = acquireMinecraftToken(xsts[0], xsts[1]); notifyProgress(progressListener, 5); - fetchOwnedItems(mcToken); checkMcProfile(mcToken); + fetchOwnedItems(mcToken); + + if (!hasProfile && doesOwnGame) { + throw new PresentedException(R.string.minecraft_no_username_set); + } else if (!doesOwnGame) { + mcName = "Demo.Player"; + mcUuid = "00000000-0000-0000-0000-000000000000"; + } else if (mcName == null || mcUuid == null) + throw new IllegalStateException("This should never happen, please report this as a bug"); MinecraftAccount acc = MinecraftAccount.load(mcName); if(acc == null) acc = new MinecraftAccount(); @@ -256,50 +265,108 @@ public class MicrosoftBackgroundLogin { } private void fetchOwnedItems(String mcAccessToken) throws IOException { + // We only need to do this if user does not have a profile/username yet + if (hasProfile) return; URL url = new URL(mcStoreUrl); + String s = ""; - HttpURLConnection conn = (HttpURLConnection)url.openConnection(); - conn.setRequestProperty("Authorization", "Bearer " + mcAccessToken); - conn.setUseCaches(false); - conn.connect(); - if(conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) { - throw getResponseThrowable(conn); + // For some reason, minecraftservices APIs are significantly more unreliable + // Automatically retry because the user gets annoyed when they have to log in again + for (int retryCount = 0; retryCount < 5; ++retryCount) { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("Authorization", "Bearer " + mcAccessToken); + conn.setUseCaches(false); + conn.connect(); + if (conn.getResponseCode() >= 200 && conn.getResponseCode() < 300) { + s = Tools.read(conn.getInputStream()); + conn.disconnect(); + break; + } else if (retryCount == 4) { + throw getResponseThrowable(conn); + } + try { Thread.sleep(500L * (1L << retryCount)); // 0.5s, 1s, 2s, 4s, 8s + } catch (InterruptedException ignored) {} } - // We don't need any data from this request, it just needs to happen in order for - // the MS servers to work properly. The data from this is practically useless - // as it does not indicate whether the user owns the game through Game Pass. + try { + String jwtSignature = new JSONObject(s).getString("signature"); + String jwtBody = jwtSignature.split("\\.")[1]; + + String signature = new String( + Base64.decode(jwtBody, Base64.DEFAULT), + StandardCharsets.UTF_8 + ); + JSONObject jsonSignature = new JSONObject(signature); + JSONArray entitlements = jsonSignature.getJSONArray("entitlements"); + for (int i = 0; i < entitlements.length(); ++i) { + switch (entitlements.getString(i)) { + // These four are guaranteed to always be present because Java & Bedrock are 1 pack + case "product_minecraft": + case "game_minecraft": + case "product_minecraft_bedrock": + case "game_minecraft_bedrock": + doesOwnGame = true; + break; + case "product_game_pass_pc": + case "product_game_pass_ultimate": + // TODO: Implement gamepass detection + break; + // idk, pad the LoC or sm + case "product_dungeons": + case "game_dungeons": + case "product_legends": + case "game_legends": + default: + break; + } + } + } + catch (JSONException e){ + Log.w("MicrosoftLogin", "Either the Auth API was changed or this account does not own Minecraft! Assuming the latter."); + doesOwnGame = false; + } + } private void checkMcProfile(String mcAccessToken) throws IOException, JSONException { URL url = new URL(mcProfileUrl); - HttpURLConnection conn = (HttpURLConnection)url.openConnection(); - conn.setRequestProperty("Authorization", "Bearer " + mcAccessToken); - conn.setUseCaches(false); - conn.connect(); + // For some reason, minecraftservices APIs are significantly more unreliable + // Automatically retry because the user gets annoyed when they have to log in again + for (int retryCount = 0; retryCount < 5; ++retryCount) { + HttpURLConnection conn = (HttpURLConnection)url.openConnection(); + conn.setRequestProperty("Authorization", "Bearer " + mcAccessToken); + conn.setUseCaches(false); + conn.connect(); - if(conn.getResponseCode() >= 200 && conn.getResponseCode() < 300) { - String s= Tools.read(conn.getInputStream()); - conn.disconnect(); - Log.i("MicrosoftLogin","profile:" + s); - JSONObject jsonObject = new JSONObject(s); - String name = (String) jsonObject.get("name"); - String uuid = (String) jsonObject.get("id"); - String uuidDashes = uuid.replaceFirst( - "(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5" - ); - doesOwnGame = true; - Log.i("MicrosoftLogin","UserName = " + name); - Log.i("MicrosoftLogin","Uuid Minecraft = " + uuidDashes); - mcName=name; - mcUuid=uuidDashes; - }else{ - Log.i("MicrosoftLogin","It seems that this Microsoft Account does not own the game."); - doesOwnGame = false; - mcName = "Demo.Player"; - mcUuid = "00000000-0000-0000-0000-000000000000"; - //throw new PresentedException(new RuntimeException(conn.getResponseMessage()), R.string.minecraft_not_owned); - //throwResponseError(conn); + if(conn.getResponseCode() >= 200 && conn.getResponseCode() < 300) { + String s= Tools.read(conn.getInputStream()); + conn.disconnect(); + Log.i("MicrosoftLogin","profile:" + s); + JSONObject jsonObject = new JSONObject(s); + String name = (String) jsonObject.get("name"); + String uuid = (String) jsonObject.get("id"); + String uuidDashes = uuid.replaceFirst( + "(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5" + ); + doesOwnGame = true; + hasProfile = true; + Log.i("MicrosoftLogin","UserName = " + name); + Log.i("MicrosoftLogin","Uuid Minecraft = " + uuidDashes); + mcName = name; + mcUuid = uuidDashes; + break; + } else if (conn.getResponseCode() == 404){ + Log.i("MicrosoftLogin","It seems that this Microsoft Account does not have a Minecraft profile, checking for ownership."); + hasProfile = false; + break; + } else if (conn.getResponseCode() == 401){ + Log.e("MicrosoftLogin", "You screwed up the auth code somewhere"); + throw getResponseThrowable(conn); + } else if (retryCount == 4) { + throw getResponseThrowable(conn); + } + try { Thread.sleep(500L * (1L << retryCount)); // 0.5s, 1s, 2s, 4s, 8s + } catch (InterruptedException ignored) {} } } diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index d0a18e2f0..2c9844cba 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -507,5 +507,6 @@ Installing… Error! The key assigned to this button is invalid. Did you try updating? Error! Invalid value! + Your Minecraft username is not set! You must set it in the Minecraft website before being able to log in!