mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-15 14:56:05 -04:00
fix: Microsoft Account refresh token
This commit is contained in:
parent
5890f0c782
commit
3b8a0989de
@ -50,12 +50,6 @@ public class MicrosoftAuthenticationServer extends NanoHTTPD implements Microsof
|
|||||||
return String.format("http://localhost:%d/auth-response", port);
|
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
|
@Override
|
||||||
public String waitFor() throws InterruptedException, ExecutionException {
|
public String waitFor() throws InterruptedException, ExecutionException {
|
||||||
return future.get();
|
return future.get();
|
||||||
@ -113,5 +107,18 @@ public class MicrosoftAuthenticationServer extends NanoHTTPD implements Microsof
|
|||||||
// TODO: error!
|
// TODO: error!
|
||||||
FXUtils.openLink(url);
|
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(""));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
|||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -79,15 +80,15 @@ public class MicrosoftAccount extends Account {
|
|||||||
if (service.validate(session.getTokenType(), session.getAccessToken())) {
|
if (service.validate(session.getTokenType(), session.getAccessToken())) {
|
||||||
authenticated = true;
|
authenticated = true;
|
||||||
} else {
|
} else {
|
||||||
MicrosoftSession acquiredSession = service.authenticate();
|
MicrosoftSession acquiredSession = service.refresh(session);
|
||||||
if (acquiredSession.getProfile() == null) {
|
if (!Objects.equals(acquiredSession.getProfile().getId(), session.getProfile().getId())) {
|
||||||
session = service.refresh(acquiredSession);
|
throw new ServerResponseMalformedException("Selected profile changed");
|
||||||
} else {
|
|
||||||
session = acquiredSession;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
characterUUID = session.getProfile().getId();
|
session = acquiredSession;
|
||||||
|
|
||||||
authenticated = true;
|
authenticated = true;
|
||||||
|
invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,6 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
|||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
public class MicrosoftService {
|
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 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 ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
|
||||||
private static final String SCOPE = "XboxLive.signin offline_access";
|
private static final String SCOPE = "XboxLive.signin offline_access";
|
||||||
@ -81,27 +80,61 @@ public class MicrosoftService {
|
|||||||
// Microsoft OAuth Flow
|
// Microsoft OAuth Flow
|
||||||
OAuthSession session = callback.startServer();
|
OAuthSession session = callback.startServer();
|
||||||
callback.openBrowser(NetworkUtils.withQuery(AUTHORIZATION_URL,
|
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("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE),
|
||||||
pair("prompt", "select_account"))));
|
pair("prompt", "select_account"))));
|
||||||
String code = session.waitFor();
|
String code = session.waitFor();
|
||||||
|
|
||||||
// Authorization Code -> Token
|
// Authorization Code -> Token
|
||||||
String responseText = HttpRequest.POST("https://login.live.com/oauth20_token.srf")
|
String responseText = HttpRequest.POST(ACCESS_TOKEN_URL)
|
||||||
.form(mapOf(pair("client_id", CLIENT_ID), pair("code", code),
|
.form(mapOf(pair("client_id", callback.getClientId()), pair("code", code),
|
||||||
pair("grant_type", "authorization_code"), pair("client_secret", session.getClientSecret()),
|
pair("grant_type", "authorization_code"), pair("client_secret", callback.getClientSecret()),
|
||||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE)))
|
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE)))
|
||||||
.getString();
|
.getString();
|
||||||
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||||
LiveAuthorizationResponse.class);
|
LiveAuthorizationResponse.class);
|
||||||
|
|
||||||
|
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServerDisconnectException(e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new NoSelectedCharacterException();
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
if (e.getCause() instanceof InterruptedException) {
|
||||||
|
throw new NoSelectedCharacterException();
|
||||||
|
} else {
|
||||||
|
throw new ServerDisconnectException(e);
|
||||||
|
}
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
throw new ServerResponseMalformedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException {
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
throw new ServerResponseMalformedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MicrosoftSession authenticateViaLiveAccessToken(String liveAccessToken, String liveRefreshToken) throws IOException, JsonParseException, AuthenticationException {
|
||||||
// Authenticate with XBox Live
|
// Authenticate with XBox Live
|
||||||
XBoxLiveAuthenticationResponse xboxResponse = HttpRequest
|
XBoxLiveAuthenticationResponse xboxResponse = HttpRequest
|
||||||
.POST("https://user.auth.xboxlive.com/user/authenticate")
|
.POST("https://user.auth.xboxlive.com/user/authenticate")
|
||||||
.json(mapOf(
|
.json(mapOf(
|
||||||
pair("Properties",
|
pair("Properties",
|
||||||
mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"),
|
mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"),
|
||||||
pair("RpsTicket", "d=" + response.accessToken))),
|
pair("RpsTicket", "d=" + liveAccessToken))),
|
||||||
pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT")))
|
pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT")))
|
||||||
.accept("application/json").getJson(XBoxLiveAuthenticationResponse.class);
|
.accept("application/json").getJson(XBoxLiveAuthenticationResponse.class);
|
||||||
String uhs = (String) xboxResponse.displayClaims.xui.get(0).get("uhs");
|
String uhs = (String) xboxResponse.displayClaims.xui.get(0).get("uhs");
|
||||||
@ -144,49 +177,12 @@ public class MicrosoftService {
|
|||||||
|
|
||||||
long notAfter = minecraftResponse.expiresIn * 1000L + System.currentTimeMillis();
|
long notAfter = minecraftResponse.expiresIn * 1000L + System.currentTimeMillis();
|
||||||
|
|
||||||
// Checking Game Ownership
|
// Get Minecraft Account UUID
|
||||||
MinecraftStoreResponse storeResponse = HttpRequest
|
MinecraftProfileResponse profileResponse = getMinecraftProfile(minecraftResponse.tokenType, minecraftResponse.accessToken);
|
||||||
.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);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ServerDisconnectException(e);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new NoSelectedCharacterException();
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
if (e.getCause() instanceof InterruptedException) {
|
|
||||||
throw new NoSelectedCharacterException();
|
|
||||||
} else {
|
|
||||||
throw new ServerDisconnectException(e);
|
|
||||||
}
|
|
||||||
} catch (JsonParseException e) {
|
|
||||||
throw new ServerResponseMalformedException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
handleErrorResponse(profileResponse);
|
||||||
return new MicrosoftSession(oldSession.getTokenType(), oldSession.getAccessToken(),
|
|
||||||
oldSession.getNotAfter(), oldSession.getUser(),
|
return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, notAfter, liveRefreshToken,
|
||||||
new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
|
new MicrosoftSession.User(minecraftResponse.username), new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ServerDisconnectException(e);
|
|
||||||
} catch (JsonParseException e) {
|
|
||||||
throw new ServerResponseMalformedException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<MinecraftProfileResponse> getCompleteProfile(String authorization) throws AuthenticationException {
|
public Optional<MinecraftProfileResponse> getCompleteProfile(String authorization) throws AuthenticationException {
|
||||||
@ -202,16 +198,12 @@ public class MicrosoftService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean validate(String tokenType, String accessToken) throws AuthenticationException {
|
public boolean validate(String tokenType, String accessToken) throws AuthenticationException {
|
||||||
|
if (true) return false;
|
||||||
requireNonNull(tokenType);
|
requireNonNull(tokenType);
|
||||||
requireNonNull(accessToken);
|
requireNonNull(accessToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
getMinecraftProfile(tokenType, accessToken);
|
||||||
.authorization(String.format("%s %s", tokenType, accessToken)).filter((url, responseCode) -> {
|
|
||||||
if (responseCode / 100 == 4) {
|
|
||||||
throw new ResponseCodeException(url, responseCode);
|
|
||||||
}
|
|
||||||
}).getString();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (ResponseCodeException e) {
|
} catch (ResponseCodeException e) {
|
||||||
return false;
|
return false;
|
||||||
@ -295,6 +287,17 @@ public class MicrosoftService {
|
|||||||
String foci;
|
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 {
|
private static class XBoxLiveAuthenticationResponseDisplayClaims {
|
||||||
List<Map<Object, Object>> xui;
|
List<Map<Object, Object>> xui;
|
||||||
}
|
}
|
||||||
@ -421,14 +424,16 @@ public class MicrosoftService {
|
|||||||
* @param url OAuth url.
|
* @param url OAuth url.
|
||||||
*/
|
*/
|
||||||
void openBrowser(String url) throws IOException;
|
void openBrowser(String url) throws IOException;
|
||||||
|
|
||||||
|
String getClientId();
|
||||||
|
|
||||||
|
String getClientSecret();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OAuthSession {
|
public interface OAuthSession {
|
||||||
|
|
||||||
String getRedirectURI();
|
String getRedirectURI();
|
||||||
|
|
||||||
String getClientSecret() throws IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for authentication
|
* Wait for authentication
|
||||||
*
|
*
|
||||||
|
@ -32,13 +32,15 @@ public class MicrosoftSession {
|
|||||||
private final String tokenType;
|
private final String tokenType;
|
||||||
private final long notAfter;
|
private final long notAfter;
|
||||||
private final String accessToken;
|
private final String accessToken;
|
||||||
|
private final String refreshToken;
|
||||||
private final User user;
|
private final User user;
|
||||||
private final GameProfile profile;
|
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.tokenType = tokenType;
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
this.notAfter = notAfter;
|
this.notAfter = notAfter;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
}
|
}
|
||||||
@ -55,6 +57,10 @@ public class MicrosoftSession {
|
|||||||
return notAfter;
|
return notAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRefreshToken() {
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public String getAuthorization() {
|
public String getAuthorization() {
|
||||||
return String.format("%s %s", getTokenType(), getAccessToken());
|
return String.format("%s %s", getTokenType(), getAccessToken());
|
||||||
}
|
}
|
||||||
@ -76,18 +82,25 @@ public class MicrosoftSession {
|
|||||||
.orElseThrow(() -> new IllegalArgumentException("tokenType is missing"));
|
.orElseThrow(() -> new IllegalArgumentException("tokenType is missing"));
|
||||||
String accessToken = tryCast(storage.get("accessToken"), String.class)
|
String accessToken = tryCast(storage.get("accessToken"), String.class)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
.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);
|
Long notAfter = tryCast(storage.get("notAfter"), Long.class).orElse(0L);
|
||||||
String userId = tryCast(storage.get("userid"), String.class)
|
String userId = tryCast(storage.get("userid"), String.class)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
.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<Object, Object> toStorage() {
|
public Map<Object, Object> toStorage() {
|
||||||
requireNonNull(profile);
|
requireNonNull(profile);
|
||||||
requireNonNull(user);
|
requireNonNull(user);
|
||||||
|
|
||||||
return mapOf(pair("tokenType", tokenType), pair("accessToken", accessToken),
|
return mapOf(
|
||||||
pair("uuid", UUIDTypeAdapter.fromUUID(profile.getId())), pair("displayName", profile.getName()),
|
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));
|
pair("userid", user.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,21 +96,26 @@ public abstract class HttpRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HttpPostRequest extends HttpRequest {
|
public static final class HttpPostRequest extends HttpRequest {
|
||||||
private byte[] bytes;
|
private byte[] bytes;
|
||||||
|
|
||||||
public HttpPostRequest(URL url) {
|
public HttpPostRequest(URL url) {
|
||||||
super(url, "POST");
|
super(url, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> 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");
|
return string(payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpPostRequest form(Map<String, String> params) {
|
public final HttpPostRequest form(Map<String, String> params) {
|
||||||
return string(NetworkUtils.withQuery("", params), "application/x-www-form-urlencoded");
|
return string(NetworkUtils.withQuery("", params), "application/x-www-form-urlencoded");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public final HttpPostRequest form(Pair<String, String>... params) {
|
||||||
|
return form(mapOf(params));
|
||||||
|
}
|
||||||
|
|
||||||
public HttpPostRequest string(String payload, String contentType) {
|
public HttpPostRequest string(String payload, String contentType) {
|
||||||
bytes = payload.getBytes(UTF_8);
|
bytes = payload.getBytes(UTF_8);
|
||||||
header("Content-Length", "" + bytes.length);
|
header("Content-Length", "" + bytes.length);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user