diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java index 0946192ed..ccb3e3eba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java @@ -50,12 +50,6 @@ public class MicrosoftAuthenticationServer extends NanoHTTPD implements Microsof return String.format("http://localhost:%d/auth-response", port); } - @Override - public String getClientSecret() { - return System.getProperty("hmcl.microsoft.auth.secret", - JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse("")); - } - @Override public String waitFor() throws InterruptedException, ExecutionException { return future.get(); @@ -113,5 +107,18 @@ public class MicrosoftAuthenticationServer extends NanoHTTPD implements Microsof // TODO: error! FXUtils.openLink(url); } + + @Override + public String getClientId() { + return System.getProperty("hmcl.microsoft.auth.id", + JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse("")); + } + + @Override + public String getClientSecret() { + return System.getProperty("hmcl.microsoft.auth.secret", + JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse("")); + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java index 219b4eddc..aae5c2062 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType; import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -79,15 +80,15 @@ public class MicrosoftAccount extends Account { if (service.validate(session.getTokenType(), session.getAccessToken())) { authenticated = true; } else { - MicrosoftSession acquiredSession = service.authenticate(); - if (acquiredSession.getProfile() == null) { - session = service.refresh(acquiredSession); - } else { - session = acquiredSession; + MicrosoftSession acquiredSession = service.refresh(session); + if (!Objects.equals(acquiredSession.getProfile().getId(), session.getProfile().getId())) { + throw new ServerResponseMalformedException("Selected profile changed"); } - characterUUID = session.getProfile().getId(); + session = acquiredSession; + authenticated = true; + invalidate(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java index fc027d181..a9d1bba51 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java @@ -48,7 +48,6 @@ import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; public class MicrosoftService { - private static final String CLIENT_ID = "6a3728d6-27a3-4180-99bb-479895b8f88e"; private static final String AUTHORIZATION_URL = "https://login.live.com/oauth20_authorize.srf"; private static final String ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf"; private static final String SCOPE = "XboxLive.signin offline_access"; @@ -81,81 +80,21 @@ public class MicrosoftService { // Microsoft OAuth Flow OAuthSession session = callback.startServer(); callback.openBrowser(NetworkUtils.withQuery(AUTHORIZATION_URL, - mapOf(pair("client_id", CLIENT_ID), pair("response_type", "code"), + mapOf(pair("client_id", callback.getClientId()), pair("response_type", "code"), pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE), pair("prompt", "select_account")))); String code = session.waitFor(); // Authorization Code -> Token - String responseText = HttpRequest.POST("https://login.live.com/oauth20_token.srf") - .form(mapOf(pair("client_id", CLIENT_ID), pair("code", code), - pair("grant_type", "authorization_code"), pair("client_secret", session.getClientSecret()), + String responseText = HttpRequest.POST(ACCESS_TOKEN_URL) + .form(mapOf(pair("client_id", callback.getClientId()), pair("code", code), + pair("grant_type", "authorization_code"), pair("client_secret", callback.getClientSecret()), pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE))) .getString(); LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText, LiveAuthorizationResponse.class); - // Authenticate with XBox Live - XBoxLiveAuthenticationResponse xboxResponse = HttpRequest - .POST("https://user.auth.xboxlive.com/user/authenticate") - .json(mapOf( - pair("Properties", - mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"), - pair("RpsTicket", "d=" + response.accessToken))), - pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT"))) - .accept("application/json").getJson(XBoxLiveAuthenticationResponse.class); - String uhs = (String) xboxResponse.displayClaims.xui.get(0).get("uhs"); - - // Authenticate Minecraft with XSTS - XBoxLiveAuthenticationResponse minecraftXstsResponse = HttpRequest - .POST("https://xsts.auth.xboxlive.com/xsts/authorize") - .json(mapOf( - pair("Properties", - mapOf(pair("SandboxId", "RETAIL"), - pair("UserTokens", Collections.singletonList(xboxResponse.token)))), - pair("RelyingParty", "rp://api.minecraftservices.com/"), pair("TokenType", "JWT"))) - .getJson(XBoxLiveAuthenticationResponse.class); - String minecraftXstsUhs = (String) minecraftXstsResponse.displayClaims.xui.get(0).get("uhs"); - if (!Objects.equals(uhs, minecraftXstsUhs)) { - throw new ServerResponseMalformedException("uhs mismatched"); - } - - // Authenticate XBox with XSTS - XBoxLiveAuthenticationResponse xboxXstsResponse = HttpRequest - .POST("https://xsts.auth.xboxlive.com/xsts/authorize") - .json(mapOf( - pair("Properties", - mapOf(pair("SandboxId", "RETAIL"), - pair("UserTokens", Collections.singletonList(xboxResponse.token)))), - pair("RelyingParty", "http://xboxlive.com"), pair("TokenType", "JWT"))) - .getJson(XBoxLiveAuthenticationResponse.class); - String xboxXstsUhs = (String) xboxXstsResponse.displayClaims.xui.get(0).get("uhs"); - if (!Objects.equals(uhs, xboxXstsUhs)) { - throw new ServerResponseMalformedException("uhs mismatched"); - } - - getXBoxProfile(uhs, xboxXstsResponse.token); - - // Authenticate with Minecraft - MinecraftLoginWithXBoxResponse minecraftResponse = HttpRequest - .POST("https://api.minecraftservices.com/authentication/login_with_xbox") - .json(mapOf(pair("identityToken", "XBL3.0 x=" + uhs + ";" + minecraftXstsResponse.token))) - .accept("application/json").getJson(MinecraftLoginWithXBoxResponse.class); - - long notAfter = minecraftResponse.expiresIn * 1000L + System.currentTimeMillis(); - - // Checking Game Ownership - MinecraftStoreResponse storeResponse = HttpRequest - .GET("https://api.minecraftservices.com/entitlements/mcstore") - .authorization(String.format("%s %s", minecraftResponse.tokenType, minecraftResponse.accessToken)) - .getJson(MinecraftStoreResponse.class); - handleErrorResponse(storeResponse); - if (storeResponse.items.isEmpty()) { - throw new NoCharacterException(); - } - - return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, notAfter, - new MicrosoftSession.User(minecraftResponse.username), null); + return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken); } catch (IOException e) { throw new ServerDisconnectException(e); } catch (InterruptedException e) { @@ -173,15 +112,14 @@ public class MicrosoftService { public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException { try { - // Get the profile - MinecraftProfileResponse profileResponse = HttpRequest - .GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile")) - .authorization(String.format("%s %s", oldSession.getTokenType(), oldSession.getAccessToken())) - .getJson(MinecraftProfileResponse.class); - handleErrorResponse(profileResponse); - return new MicrosoftSession(oldSession.getTokenType(), oldSession.getAccessToken(), - oldSession.getNotAfter(), oldSession.getUser(), - new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name)); + LiveRefreshResponse response = HttpRequest.POST(ACCESS_TOKEN_URL) + .form(pair("client_id", callback.getClientId()), + pair("client_secret", callback.getClientSecret()), + pair("refresh_token", oldSession.getRefreshToken()), + pair("grant_type", "refresh_token")) + .accept("application/json").getJson(LiveRefreshResponse.class); + + return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken); } catch (IOException e) { throw new ServerDisconnectException(e); } catch (JsonParseException e) { @@ -189,6 +127,64 @@ public class MicrosoftService { } } + private MicrosoftSession authenticateViaLiveAccessToken(String liveAccessToken, String liveRefreshToken) throws IOException, JsonParseException, AuthenticationException { + // Authenticate with XBox Live + XBoxLiveAuthenticationResponse xboxResponse = HttpRequest + .POST("https://user.auth.xboxlive.com/user/authenticate") + .json(mapOf( + pair("Properties", + mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"), + pair("RpsTicket", "d=" + liveAccessToken))), + pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT"))) + .accept("application/json").getJson(XBoxLiveAuthenticationResponse.class); + String uhs = (String) xboxResponse.displayClaims.xui.get(0).get("uhs"); + + // Authenticate Minecraft with XSTS + XBoxLiveAuthenticationResponse minecraftXstsResponse = HttpRequest + .POST("https://xsts.auth.xboxlive.com/xsts/authorize") + .json(mapOf( + pair("Properties", + mapOf(pair("SandboxId", "RETAIL"), + pair("UserTokens", Collections.singletonList(xboxResponse.token)))), + pair("RelyingParty", "rp://api.minecraftservices.com/"), pair("TokenType", "JWT"))) + .getJson(XBoxLiveAuthenticationResponse.class); + String minecraftXstsUhs = (String) minecraftXstsResponse.displayClaims.xui.get(0).get("uhs"); + if (!Objects.equals(uhs, minecraftXstsUhs)) { + throw new ServerResponseMalformedException("uhs mismatched"); + } + + // Authenticate XBox with XSTS + XBoxLiveAuthenticationResponse xboxXstsResponse = HttpRequest + .POST("https://xsts.auth.xboxlive.com/xsts/authorize") + .json(mapOf( + pair("Properties", + mapOf(pair("SandboxId", "RETAIL"), + pair("UserTokens", Collections.singletonList(xboxResponse.token)))), + pair("RelyingParty", "http://xboxlive.com"), pair("TokenType", "JWT"))) + .getJson(XBoxLiveAuthenticationResponse.class); + String xboxXstsUhs = (String) xboxXstsResponse.displayClaims.xui.get(0).get("uhs"); + if (!Objects.equals(uhs, xboxXstsUhs)) { + throw new ServerResponseMalformedException("uhs mismatched"); + } + + getXBoxProfile(uhs, xboxXstsResponse.token); + + // Authenticate with Minecraft + MinecraftLoginWithXBoxResponse minecraftResponse = HttpRequest + .POST("https://api.minecraftservices.com/authentication/login_with_xbox") + .json(mapOf(pair("identityToken", "XBL3.0 x=" + uhs + ";" + minecraftXstsResponse.token))) + .accept("application/json").getJson(MinecraftLoginWithXBoxResponse.class); + + long notAfter = minecraftResponse.expiresIn * 1000L + System.currentTimeMillis(); + + // Get Minecraft Account UUID + MinecraftProfileResponse profileResponse = getMinecraftProfile(minecraftResponse.tokenType, minecraftResponse.accessToken); + handleErrorResponse(profileResponse); + + return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, notAfter, liveRefreshToken, + new MicrosoftSession.User(minecraftResponse.username), new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name)); + } + public Optional getCompleteProfile(String authorization) throws AuthenticationException { try { return Optional.ofNullable( @@ -202,16 +198,12 @@ public class MicrosoftService { } public boolean validate(String tokenType, String accessToken) throws AuthenticationException { + if (true) return false; requireNonNull(tokenType); requireNonNull(accessToken); try { - HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile")) - .authorization(String.format("%s %s", tokenType, accessToken)).filter((url, responseCode) -> { - if (responseCode / 100 == 4) { - throw new ResponseCodeException(url, responseCode); - } - }).getString(); + getMinecraftProfile(tokenType, accessToken); return true; } catch (ResponseCodeException e) { return false; @@ -295,6 +287,17 @@ public class MicrosoftService { String foci; } + private static class LiveRefreshResponse { + @SerializedName("expires_in") + int expiresIn; + + @SerializedName("access_token") + String accessToken; + + @SerializedName("refresh_token") + String refreshToken; + } + private static class XBoxLiveAuthenticationResponseDisplayClaims { List> xui; } @@ -421,14 +424,16 @@ public class MicrosoftService { * @param url OAuth url. */ void openBrowser(String url) throws IOException; + + String getClientId(); + + String getClientSecret(); } public interface OAuthSession { String getRedirectURI(); - String getClientSecret() throws IOException; - /** * Wait for authentication * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java index 87d3375e2..755c68ce2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java @@ -32,13 +32,15 @@ public class MicrosoftSession { private final String tokenType; private final long notAfter; private final String accessToken; + private final String refreshToken; private final User user; private final GameProfile profile; - public MicrosoftSession(String tokenType, String accessToken, long notAfter, User user, GameProfile profile) { + public MicrosoftSession(String tokenType, String accessToken, long notAfter, String refreshToken, User user, GameProfile profile) { this.tokenType = tokenType; this.accessToken = accessToken; this.notAfter = notAfter; + this.refreshToken = refreshToken; this.user = user; this.profile = profile; } @@ -55,6 +57,10 @@ public class MicrosoftSession { return notAfter; } + public String getRefreshToken() { + return refreshToken; + } + public String getAuthorization() { return String.format("%s %s", getTokenType(), getAccessToken()); } @@ -76,18 +82,25 @@ public class MicrosoftSession { .orElseThrow(() -> new IllegalArgumentException("tokenType is missing")); String accessToken = tryCast(storage.get("accessToken"), String.class) .orElseThrow(() -> new IllegalArgumentException("accessToken is missing")); + String refreshToken = tryCast(storage.get("refreshToken"), String.class) + .orElseThrow(() -> new IllegalArgumentException("refreshToken is missing")); Long notAfter = tryCast(storage.get("notAfter"), Long.class).orElse(0L); String userId = tryCast(storage.get("userid"), String.class) .orElseThrow(() -> new IllegalArgumentException("userid is missing")); - return new MicrosoftSession(tokenType, accessToken, notAfter, new User(userId), new GameProfile(uuid, name)); + return new MicrosoftSession(tokenType, accessToken, notAfter, refreshToken, new User(userId), new GameProfile(uuid, name)); } public Map toStorage() { requireNonNull(profile); requireNonNull(user); - return mapOf(pair("tokenType", tokenType), pair("accessToken", accessToken), - pair("uuid", UUIDTypeAdapter.fromUUID(profile.getId())), pair("displayName", profile.getName()), + return mapOf( + pair("uuid", UUIDTypeAdapter.fromUUID(profile.getId())), + pair("displayName", profile.getName()), + pair("tokenType", tokenType), + pair("accessToken", accessToken), + pair("refreshToken", refreshToken), + pair("notAfter", notAfter), pair("userid", user.id)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java index 95a882d56..f474b67ec 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java @@ -96,21 +96,26 @@ public abstract class HttpRequest { } } - public static class HttpPostRequest extends HttpRequest { + public static final class HttpPostRequest extends HttpRequest { private byte[] bytes; public HttpPostRequest(URL url) { super(url, "POST"); } - public HttpPostRequest json(Object payload) throws JsonParseException { + public HttpPostRequest json(Object payload) throws JsonParseException { return string(payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json"); } - public HttpPostRequest form(Map params) { + public final HttpPostRequest form(Map params) { return string(NetworkUtils.withQuery("", params), "application/x-www-form-urlencoded"); } + @SafeVarargs + public final HttpPostRequest form(Pair... params) { + return form(mapOf(params)); + } + public HttpPostRequest string(String payload, String contentType) { bytes = payload.getBytes(UTF_8); header("Content-Length", "" + bytes.length);