@@ -127,13 +127,23 @@ public static void loginWithToken(String token) throws LoginException
127127 public static void loginWithRefreshToken (String refreshToken )
128128 throws LoginException
129129 {
130- System .out .println ("Logging in with refresh token..." );
130+ loginWithRefreshToken (refreshToken , null );
131+ }
132+
133+ public static void loginWithRefreshToken (String refreshToken ,
134+ String clientId ) throws LoginException
135+ {
136+ boolean hasCustomClient = clientId != null && !clientId .isBlank ()
137+ && !clientId .equals (CLIENT_ID );
138+ System .out .println ("Logging in with refresh token" + (hasCustomClient
139+ ? " (custom client_id: " + clientId + ")" : " (default client_id)" )
140+ + "..." );
131141 long startTime = System .nanoTime ();
132142
133143 try
134144 {
135145 MinecraftProfile mcProfile =
136- authenticateRefreshTokenWithoutSession (refreshToken );
146+ authenticateRefreshTokenWithoutSession (refreshToken , clientId );
137147 System .out .println ("Refresh-token auth resolved profile: "
138148 + mcProfile .getName () + " (" + mcProfile .getUUID () + ")" );
139149 setSession (mcProfile );
@@ -145,6 +155,8 @@ public static void loginWithRefreshToken(String refreshToken)
145155 {
146156 System .out .println ("Refresh-token login failed after "
147157 + (System .nanoTime () - startTime ) / 1e6D + " ms" );
158+ System .out .println ("Exception: " + e .getMessage ());
159+ e .printStackTrace ();
148160 throw e ;
149161 }
150162 }
@@ -171,22 +183,36 @@ public static MinecraftProfile authenticateTokenWithoutSession(String token)
171183
172184 public static MinecraftProfile authenticateRefreshTokenWithoutSession (
173185 String refreshToken ) throws LoginException
186+ {
187+ return authenticateRefreshTokenWithoutSession (refreshToken , null );
188+ }
189+
190+ public static MinecraftProfile authenticateRefreshTokenWithoutSession (
191+ String refreshToken , String clientId ) throws LoginException
174192 {
175193 if (refreshToken == null || refreshToken .isBlank ())
176194 throw new LoginException ("Refresh token cannot be empty." );
177195
178- String msftAccessToken =
179- getMicrosoftAccessTokenFromRefreshToken ( refreshToken .trim ());
196+ String msftAccessToken = getMicrosoftAccessTokenFromRefreshToken (
197+ refreshToken .trim (), clientId );
180198 return getAccountFromMicrosoftAccessToken (msftAccessToken );
181199 }
182200
183201 public static MinecraftProfile authenticateTokenAltWithoutSession (
184202 String token , String refreshToken ) throws LoginException
203+ {
204+ return authenticateTokenAltWithoutSession (token , refreshToken , null );
205+ }
206+
207+ public static MinecraftProfile authenticateTokenAltWithoutSession (
208+ String token , String refreshToken , String clientId )
209+ throws LoginException
185210 {
186211 String trimmedRefresh = refreshToken == null ? "" : refreshToken .trim ();
187212
188213 if (!trimmedRefresh .isEmpty ())
189- return authenticateRefreshTokenWithoutSession (trimmedRefresh );
214+ return authenticateRefreshTokenWithoutSession (trimmedRefresh ,
215+ clientId );
190216
191217 return authenticateTokenWithoutSession (token );
192218 }
@@ -394,13 +420,33 @@ private static String getMicrosoftAccessToken(String authCode)
394420 private static String getMicrosoftAccessTokenFromRefreshToken (
395421 String refreshToken ) throws LoginException
396422 {
423+ return getMicrosoftAccessTokenFromRefreshToken (refreshToken , null );
424+ }
425+
426+ private static String getMicrosoftAccessTokenFromRefreshToken (
427+ String refreshToken , String clientIdOverride ) throws LoginException
428+ {
429+ String effectiveClientId =
430+ clientIdOverride != null && !clientIdOverride .isBlank ()
431+ ? clientIdOverride .trim () : CLIENT_ID ;
432+
433+ boolean isNonDefaultClient = !effectiveClientId .equals (CLIENT_ID );
434+
435+ System .out .println ("Using client_id: " + effectiveClientId );
436+
397437 Map <String , String > postData = new HashMap <>();
398- postData .put ("client_id" , CLIENT_ID );
438+ postData .put ("client_id" , effectiveClientId );
399439 postData .put ("refresh_token" , refreshToken );
400440 postData .put ("grant_type" , "refresh_token" );
401- postData .put ("redirect_uri" ,
402- "https://login.live.com/oauth20_desktop.srf" );
403- postData .put ("scope" , SCOPE_UNENCODED );
441+
442+ // When using a non-default client, omit scope and redirect_uri
443+ // to avoid mismatches with how the token was originally obtained.
444+ if (!isNonDefaultClient )
445+ {
446+ postData .put ("redirect_uri" ,
447+ "https://login.live.com/oauth20_desktop.srf" );
448+ postData .put ("scope" , SCOPE_UNENCODED );
449+ }
404450
405451 byte [] encodedDataBytes =
406452 urlEncodeMap (postData ).getBytes (StandardCharsets .UTF_8 );
@@ -422,6 +468,16 @@ private static String getMicrosoftAccessTokenFromRefreshToken(
422468 out .write (encodedDataBytes );
423469 }
424470
471+ int responseCode = connection .getResponseCode ();
472+ if (responseCode != 200 )
473+ {
474+ String errorBody = readErrorBody (connection );
475+ System .out .println ("Microsoft token refresh returned HTTP "
476+ + responseCode + ": " + errorBody );
477+ throw new LoginException ("Microsoft returned HTTP "
478+ + responseCode + ": " + errorBody );
479+ }
480+
425481 WsonObject json = JsonUtils .parseConnectionToObject (connection );
426482 return json .getString ("access_token" );
427483
@@ -435,13 +491,36 @@ private static String getMicrosoftAccessTokenFromRefreshToken(
435491 }
436492 }
437493
494+ private static String readErrorBody (HttpURLConnection connection )
495+ {
496+ try (InputStream errorStream = connection .getErrorStream ())
497+ {
498+ if (errorStream == null )
499+ return connection .getResponseMessage ();
500+
501+ try (BufferedReader reader = new BufferedReader (
502+ new InputStreamReader (errorStream , StandardCharsets .UTF_8 )))
503+ {
504+ return reader .lines ().collect (Collectors .joining ());
505+ }
506+ }catch (IOException e )
507+ {
508+ return "(could not read error body)" ;
509+ }
510+ }
511+
438512 private static XBoxLiveToken getXBLToken (String msftAccessToken )
439513 throws LoginException
440514 {
441515 JsonObject properties = new JsonObject ();
442516 properties .addProperty ("AuthMethod" , "RPS" );
443517 properties .addProperty ("SiteName" , "user.auth.xboxlive.com" );
444- properties .addProperty ("RpsTicket" , msftAccessToken );
518+
519+ // The RpsTicket must be the access token prefixed with "d="
520+ // to indicate it's a delegated token from Microsoft account.
521+ String rpsTicket = msftAccessToken .startsWith ("d=" ) ? msftAccessToken
522+ : "d=" + msftAccessToken ;
523+ properties .addProperty ("RpsTicket" , rpsTicket );
445524
446525 JsonObject postData = new JsonObject ();
447526 postData .addProperty ("RelyingParty" , "http://auth.xboxlive.com" );
@@ -452,7 +531,8 @@ private static XBoxLiveToken getXBLToken(String msftAccessToken)
452531
453532 try
454533 {
455- URLConnection connection = XBL_TOKEN_URL .openConnection ();
534+ HttpURLConnection connection =
535+ (HttpURLConnection )XBL_TOKEN_URL .openConnection ();
456536
457537 connection .setRequestProperty ("Content-Type" , "application/json" );
458538 connection .setRequestProperty ("Accept" , "application/json" );
@@ -466,6 +546,16 @@ private static XBoxLiveToken getXBLToken(String msftAccessToken)
466546 out .write (request .getBytes (StandardCharsets .US_ASCII ));
467547 }
468548
549+ int responseCode = connection .getResponseCode ();
550+ if (responseCode != 200 )
551+ {
552+ String errorBody = readErrorBody (connection );
553+ System .out .println ("XBL authentication returned HTTP "
554+ + responseCode + ": " + errorBody );
555+ throw new LoginException ("XBL auth returned HTTP "
556+ + responseCode + ": " + errorBody );
557+ }
558+
469559 WsonObject json = JsonUtils .parseConnectionToObject (connection );
470560
471561 String token = json .getString ("Token" );
0 commit comments