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);
|
||||
}
|
||||
|
||||
@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(""));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,27 +80,61 @@ 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);
|
||||
|
||||
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
|
||||
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("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");
|
||||
@ -144,49 +177,12 @@ public class MicrosoftService {
|
||||
|
||||
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);
|
||||
} 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);
|
||||
// Get Minecraft Account UUID
|
||||
MinecraftProfileResponse profileResponse = getMinecraftProfile(minecraftResponse.tokenType, minecraftResponse.accessToken);
|
||||
handleErrorResponse(profileResponse);
|
||||
return new MicrosoftSession(oldSession.getTokenType(), oldSession.getAccessToken(),
|
||||
oldSession.getNotAfter(), oldSession.getUser(),
|
||||
new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
|
||||
return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, notAfter, liveRefreshToken,
|
||||
new MicrosoftSession.User(minecraftResponse.username), new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
|
||||
}
|
||||
|
||||
public Optional<MinecraftProfileResponse> getCompleteProfile(String authorization) throws AuthenticationException {
|
||||
@ -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<Map<Object, Object>> 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
|
||||
*
|
||||
|
@ -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<Object, Object> 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));
|
||||
}
|
||||
|
||||
|
@ -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 <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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final HttpPostRequest form(Pair<String, String>... params) {
|
||||
return form(mapOf(params));
|
||||
}
|
||||
|
||||
public HttpPostRequest string(String payload, String contentType) {
|
||||
bytes = payload.getBytes(UTF_8);
|
||||
header("Content-Length", "" + bytes.length);
|
||||
|
Loading…
x
Reference in New Issue
Block a user