Untitled
java
2 months ago
19 kB
1
Indexable
Never
/* * Copyright (C) 2023 Kawaxte * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this * program. If not, see <https://www.gnu.org/licenses/>. */ package ch.kawaxte.launcher.auth; import ch.kawaxte.launcher.ui.LauncherNoNetworkPanel; import ch.kawaxte.launcher.ui.LauncherPanel; import ch.kawaxte.launcher.ui.MicrosoftAuthPanel; import ch.kawaxte.launcher.util.LauncherLanguageUtils; import ch.kawaxte.launcher.util.LauncherUtils; import com.google.api.client.http.ByteArrayContent; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.util.GenericData; import java.io.IOException; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import javax.swing.JProgressBar; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class providing a range of static methods to handle different stages of the OAuth2 flow, * including acquiring and refreshing tokens, making API requests to Xbox Live and Minecraft * services, and retrieving the user's Minecraft profile. * * <p>Note that this class is a singleton, and thus cannot be instantiated directly. * * @author Kawaxte * @since 1.5.0923_03 * @see <a * href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code"> * OAuth 2.0 device authorization grant</a> */ public final class MicrosoftAuth { private static final Logger LOGGER; static { LOGGER = LoggerFactory.getLogger(MicrosoftAuth.class); } private MicrosoftAuth() {} /** * Initialises and returns URLs for Microsoft OAuth2 authentication process. * * @return Array of URLs necessary for the OAuth2 flow */ private static GenericUrl[] getGenericUrls() { GenericUrl[] urls = new GenericUrl[7]; urls[0] = new GenericUrl( new StringBuilder() .append("https://login.microsoftonline.com/") .append("consumers/") .append("oauth2/") .append("v2.0/") .append("devicecode") .toString()); urls[1] = new GenericUrl( new StringBuilder() .append("https://login.microsoftonline.com/") .append("consumers/") .append("oauth2/") .append("v2.0/") .append("token") .toString()); urls[2] = new GenericUrl( new StringBuilder() .append("https://user.auth.xboxlive.com/") .append("user/") .append("authenticate") .toString()); urls[3] = new GenericUrl( new StringBuilder() .append("https://xsts.auth.xboxlive.com/") .append("xsts/") .append("authorize") .toString()); urls[4] = new GenericUrl( new StringBuilder() .append("https://api.minecraftservices.com/") .append("authentication/") .append("login_with_xbox") .toString()); urls[5] = new GenericUrl( new StringBuilder() .append("https://api.minecraftservices.com/") .append("entitlements/") .append("mcstore") .toString()); urls[6] = new GenericUrl( new StringBuilder() .append("https://api.minecraftservices.com/") .append("minecraft/") .append("profile") .toString()); return urls; } /** * Acquires a device code that will be used to retrieve both the user's access token and refresh * token. * * <p>This method is required to be polled every second until the user has entered the code on the * provided URL. The polling process is handled by {@link MicrosoftAuthWorker} * * @param clientId The client ID of the Azure application requesting the device code * @return A {@link JSONObject} containing the response from the server */ public static JSONObject acquireDeviceCode(String clientId) { GenericData data = new GenericData(); data.put("client_id", clientId); data.put("response_type", "code"); data.put("scope", "XboxLive.signin offline_access"); HttpTransport transport = new NetHttpTransport(); HttpContent content = new UrlEncodedContent(data); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildPostRequest(getGenericUrls()[0], content); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot acquire device code", ioe); } return null; } /** * Acquires both the user's access token and refresh token using the obtained device code. * received. * * <p>This method should be called after a successful call to {@link #acquireDeviceCode(String)}. * * @param clientId The client ID of the Azure application requesting the access token * @param deviceCode The device code received from {@link #acquireDeviceCode(String)} * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject acquireToken(String clientId, String deviceCode) { GenericData data = new GenericData(); data.put("client_id", clientId); data.put("device_code", deviceCode); data.put("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); HttpTransport transport = new NetHttpTransport(); HttpContent content = new UrlEncodedContent(data); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildPostRequest(getGenericUrls()[1], content); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { String message = ioe.getMessage(); message = message.substring(message.indexOf("\n") + 1); message = message.substring(message.indexOf("\n") + 1); JSONObject jsonMessage = new JSONObject(message); handleTokenException(ioe, jsonMessage); } return null; } /** * Handles any message(s) received from the server when attempting to acquire a token. * * @param ioe a {@link IOException} thrown when attempting to acquire a token * @param object a {@link JSONObject} containing the response from the server */ private static void handleTokenException(IOException ioe, JSONObject object) { if (object.has("error")) { String error = object.getString("error"); switch (error) { case "authorization_pending": JProgressBar progressBar = MicrosoftAuthPanel.getInstance().getExpiresInProgressBar(); progressBar.setValue(progressBar.getValue() - 1); break; case "invalid_grant": break; default: LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot acquire token", ioe); break; } } } /** * Refreshes the user's access token and refresh token using a previously obtained refresh token. * * <p>Upon a successful refresh, the new access token and refresh token will be returned in the * response, which can then be used to re-obtain the Minecraft access token. * * @param clientId The client ID of the Azure application requesting the access token * @param refreshToken The refresh token of the user * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject refreshToken(String clientId, String refreshToken) { GenericData data = new GenericData(); data.put("client_id", clientId); data.put("refresh_token", refreshToken); data.put("grant_type", "refresh_token"); HttpTransport transport = new NetHttpTransport(); HttpContent content = new UrlEncodedContent(data); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildPostRequest(getGenericUrls()[1], content); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot refresh access token", ioe); } return null; } /** * Acquires the user's Xbox Live token using an previously obtained access token. * * @param accessToken The access token of the user * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject acquireXBLToken(String accessToken) { JSONObject properties = new JSONObject(); properties.put("AuthMethod", "RPS"); properties.put("SiteName", "user.auth.xboxlive.com"); properties.put("RpsTicket", String.format("d=%s", accessToken)); JSONObject data = new JSONObject(); data.put("Properties", properties); data.put("RelyingParty", "http://auth.xboxlive.com"); data.put("TokenType", "JWT"); HttpTransport transport = new NetHttpTransport(); HttpContent content = new ByteArrayContent("application/json", data.toString().getBytes(StandardCharsets.UTF_8)); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildPostRequest(getGenericUrls()[2], content); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot acquire Xbox Live token", ioe); } return null; } /** * Acquires the user's Xbox Live XSTS token using the previously obtained Xbox Live token. * * @param token The Xbox Live token of the user * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject acquireXSTSToken(String token) { JSONObject properties = new JSONObject(); properties.put("SandboxId", "RETAIL"); properties.put("UserTokens", new String[] {token}); JSONObject data = new JSONObject(); data.put("Properties", properties); data.put("RelyingParty", "rp://api.minecraftservices.com/"); data.put("TokenType", "JWT"); HttpTransport transport = new NetHttpTransport(); HttpContent content = new ByteArrayContent("application/json", data.toString().getBytes(StandardCharsets.UTF_8)); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildPostRequest(getGenericUrls()[3], content); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot acquire XSTS token", ioe); } return null; } /** * Acquires the user's Minecraft access token using the previously obtained XSTS token. * * <p>The Minecraft access token will not only be used to construct a valid session ID, but also * to retrieve the user's profile, and to check if the user owns the minecraft. * * @param uhs The user hash * @param token The XSTS token of the user * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject acquireAccessToken(String uhs, String token) { JSONObject body = new JSONObject(); body.put("identityToken", String.format("XBL3.0 x=%s;%s", uhs, token)); body.put("ensureLegacyEnabled", true); HttpTransport transport = new NetHttpTransport(); HttpContent content = new ByteArrayContent("application/json", body.toString().getBytes(StandardCharsets.UTF_8)); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildPostRequest(getGenericUrls()[4], content); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot acquire Minecraft access token", ioe); } return null; } /** * Checks if the user owns Minecraft (Java Edition) using the previously obtained Minecraft access * token. received. If not, the user will only be able to play in an instance where their username * is randomly generated in a "Player###" format, and they will not be able to join multiplayer * servers that have {@code online-mode} enabled. * * @param accessToken The Minecraft access token of the user * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject checkEntitlementsMcStore(String accessToken) { HttpTransport transport = new NetHttpTransport(); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildGetRequest(getGenericUrls()[5]); request.getHeaders().setAuthorization(String.format("Bearer %s", accessToken)); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot check Minecraft Store entitlements", ioe); } return null; } /** * Acquires the user's Minecraft profile using the previously obtained Minecraft access token. * * <p>The Minecraft profile will be used to retrieve the user's profile information, such as their * username, UUID, skin, etc. The UUID will be used as one of the components to construct a valid * session ID. * * @param accessToken The Minecraft access token of the user * @return A {@link org.json.JSONObject} containing the response from the server */ public static JSONObject acquireMinecraftProfile(String accessToken) { HttpTransport transport = new NetHttpTransport(); HttpRequestFactory factory = transport.createRequestFactory(); try { HttpRequest request = factory.buildGetRequest(getGenericUrls()[6]); request.getHeaders().setAuthorization(String.format("Bearer %s", accessToken)); HttpResponse response = request.execute(); return new JSONObject(response.parseAsString()); } catch (UnknownHostException uhe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[1], uhe.getMessage())); LauncherUtils.setNotPremium(false); } catch (IOException ioe) { LauncherUtils.swapContainers( LauncherPanel.getInstance(), new LauncherNoNetworkPanel(LauncherLanguageUtils.getLNPPKeys()[0])); LauncherUtils.setNotPremium(true); LOGGER.error("Cannot acquire Minecraft profile", ioe); } return null; } }