mirror of
https://github.com/SKCraft/Launcher.git
synced 2024-11-24 12:16:28 +01:00
Implement Microsoft OAuth login process
This commit is contained in:
parent
3ddaea55dc
commit
d2127e9b13
@ -12,10 +12,7 @@ import com.google.common.base.Strings;
|
|||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import com.skcraft.launcher.auth.AccountList;
|
import com.skcraft.launcher.auth.*;
|
||||||
import com.skcraft.launcher.auth.LoginService;
|
|
||||||
import com.skcraft.launcher.auth.UserType;
|
|
||||||
import com.skcraft.launcher.auth.YggdrasilLoginService;
|
|
||||||
import com.skcraft.launcher.launch.LaunchSupervisor;
|
import com.skcraft.launcher.launch.LaunchSupervisor;
|
||||||
import com.skcraft.launcher.model.minecraft.Library;
|
import com.skcraft.launcher.model.minecraft.Library;
|
||||||
import com.skcraft.launcher.model.minecraft.VersionManifest;
|
import com.skcraft.launcher.model.minecraft.VersionManifest;
|
||||||
@ -166,6 +163,10 @@ public final class Launcher {
|
|||||||
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")), accounts.getClientId());
|
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")), accounts.getClientId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MicrosoftLoginService getMicrosoftLogin() {
|
||||||
|
return new MicrosoftLoginService(getProperties().getProperty("microsoftClientId"));
|
||||||
|
}
|
||||||
|
|
||||||
public LoginService getLoginService(UserType type) {
|
public LoginService getLoginService(UserType type) {
|
||||||
if (type == UserType.MICROSOFT) {
|
if (type == UserType.MICROSOFT) {
|
||||||
return null; // TODO: Microsoft login service
|
return null; // TODO: Microsoft login service
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
package com.skcraft.launcher.auth;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.MicrosoftWebAuthorizer;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.XboxTokenAuthorizer;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.model.McAuthResponse;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.model.McProfileResponse;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.model.TokenResponse;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.model.XboxAuthorization;
|
||||||
|
import com.skcraft.launcher.util.HttpRequest;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MicrosoftLoginService implements LoginService {
|
||||||
|
private static final URL MS_TOKEN_URL = url("https://login.live.com/oauth20_token.srf");
|
||||||
|
|
||||||
|
private final String clientId;
|
||||||
|
|
||||||
|
public Session login() throws IOException, InterruptedException, AuthenticationException {
|
||||||
|
MicrosoftWebAuthorizer authorizer = new MicrosoftWebAuthorizer(clientId);
|
||||||
|
String code = authorizer.authorize();
|
||||||
|
|
||||||
|
TokenResponse response = exchangeToken(form -> {
|
||||||
|
form.add("grant_type", "authorization_code");
|
||||||
|
form.add("redirect_uri", authorizer.getRedirectUri());
|
||||||
|
form.add("code", code);
|
||||||
|
});
|
||||||
|
|
||||||
|
Profile session = performLogin(response.getAccessToken());
|
||||||
|
session.setRefreshToken(response.getRefreshToken());
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Session restore(SavedSession savedSession)
|
||||||
|
throws IOException, InterruptedException, AuthenticationException {
|
||||||
|
TokenResponse response = exchangeToken(form -> {
|
||||||
|
form.add("grant_type", "refresh_token");
|
||||||
|
form.add("refresh_token", savedSession.getRefreshToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
Profile session = performLogin(response.getAccessToken());
|
||||||
|
session.setRefreshToken(response.getRefreshToken());
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenResponse exchangeToken(Consumer<HttpRequest.Form> formConsumer)
|
||||||
|
throws IOException, InterruptedException, AuthenticationException {
|
||||||
|
HttpRequest.Form form = HttpRequest.Form.form();
|
||||||
|
form.add("client_id", clientId);
|
||||||
|
formConsumer.accept(form);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.post(MS_TOKEN_URL)
|
||||||
|
.bodyForm(form)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (request.getResponseCode() == 200) {
|
||||||
|
return request.returnContent().asJson(TokenResponse.class);
|
||||||
|
} else {
|
||||||
|
TokenError error = request.returnContent().asJson(TokenError.class);
|
||||||
|
|
||||||
|
throw new AuthenticationException(error.errorDescription, error.errorDescription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Profile performLogin(String microsoftToken)
|
||||||
|
throws IOException, InterruptedException, AuthenticationException {
|
||||||
|
XboxAuthorization xboxAuthorization = XboxTokenAuthorizer.authorizeWithXbox(microsoftToken);
|
||||||
|
McAuthResponse auth = MinecraftServicesAuthorizer.authorizeWithMinecraft(xboxAuthorization);
|
||||||
|
McProfileResponse profile = MinecraftServicesAuthorizer.getUserProfile(auth);
|
||||||
|
|
||||||
|
Profile session = new Profile(auth, profile);
|
||||||
|
session.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid()));
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Profile implements Session {
|
||||||
|
private final McAuthResponse auth;
|
||||||
|
private final McProfileResponse profile;
|
||||||
|
private final Map<String, String> userProperties = Collections.emptyMap();
|
||||||
|
private String refreshToken;
|
||||||
|
private String avatarImage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUuid() {
|
||||||
|
return profile.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return profile.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccessToken() {
|
||||||
|
return auth.getAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSessionToken() {
|
||||||
|
return String.format("token:%s:%s", getAccessToken(), getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserType getUserType() {
|
||||||
|
return UserType.MICROSOFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SavedSession toSavedSession() {
|
||||||
|
SavedSession savedSession = new SavedSession();
|
||||||
|
|
||||||
|
savedSession.setType(getUserType());
|
||||||
|
savedSession.setUsername(getName());
|
||||||
|
savedSession.setUuid(getUuid());
|
||||||
|
savedSession.setAccessToken(getAccessToken());
|
||||||
|
savedSession.setRefreshToken(getRefreshToken());
|
||||||
|
savedSession.setAvatarImage(getAvatarImage());
|
||||||
|
|
||||||
|
return savedSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
private static class TokenError {
|
||||||
|
private String error;
|
||||||
|
private String errorDescription;
|
||||||
|
}
|
||||||
|
}
|
@ -37,11 +37,6 @@ public class OfflineSession implements Session {
|
|||||||
return (new UUID(0, 0)).toString();
|
return (new UUID(0, 0)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientToken() {
|
|
||||||
return "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAccessToken() {
|
public String getAccessToken() {
|
||||||
return "0";
|
return "0";
|
||||||
|
@ -27,13 +27,6 @@ public interface Session {
|
|||||||
*/
|
*/
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the client token.
|
|
||||||
*
|
|
||||||
* @return client token
|
|
||||||
*/
|
|
||||||
String getClientToken();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the access token.
|
* Get the access token.
|
||||||
*
|
*
|
||||||
|
@ -124,12 +124,6 @@ public class YggdrasilLoginService implements LoginService {
|
|||||||
return String.format("token:%s:%s", getAccessToken(), getUuid());
|
return String.format("token:%s:%s", getAccessToken(), getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@JsonIgnore
|
|
||||||
public String getClientToken() {
|
|
||||||
return response.getClientToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public String getAccessToken() {
|
public String getAccessToken() {
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft;
|
||||||
|
|
||||||
|
import com.skcraft.launcher.auth.AuthenticationException;
|
||||||
|
import com.skcraft.launcher.util.HttpRequest;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the Microsoft leg of OAuth authorization.
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MicrosoftWebAuthorizer {
|
||||||
|
private final String clientId;
|
||||||
|
@Getter private String redirectUri;
|
||||||
|
|
||||||
|
public String authorize() throws IOException, AuthenticationException, InterruptedException {
|
||||||
|
if (Desktop.isDesktopSupported()) {
|
||||||
|
// Interactive auth
|
||||||
|
return authorizeInteractive();
|
||||||
|
} else {
|
||||||
|
// TODO Device code auth
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String authorizeInteractive() throws IOException, AuthenticationException, InterruptedException {
|
||||||
|
OauthHttpHandler httpHandler = new OauthHttpHandler();
|
||||||
|
Desktop.getDesktop().browse(generateInteractiveUrl(httpHandler.getPort()));
|
||||||
|
|
||||||
|
return httpHandler.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI generateInteractiveUrl(int port) throws AuthenticationException {
|
||||||
|
redirectUri = "http://localhost:" + port;
|
||||||
|
|
||||||
|
URI interactive;
|
||||||
|
try {
|
||||||
|
HttpRequest.Form query = HttpRequest.Form.form();
|
||||||
|
query.add("client_id", clientId);
|
||||||
|
query.add("scope", "XboxLive.signin XboxLive.offline_access");
|
||||||
|
query.add("response_type", "code");
|
||||||
|
query.add("redirect_uri", redirectUri);
|
||||||
|
|
||||||
|
interactive = new URI("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?"
|
||||||
|
+ query.toString());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new AuthenticationException(e, "Failed to generate OAuth URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
return interactive;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft;
|
||||||
|
|
||||||
|
import com.skcraft.launcher.auth.AuthenticationException;
|
||||||
|
import com.skcraft.launcher.auth.microsoft.model.*;
|
||||||
|
import com.skcraft.launcher.util.HttpRequest;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||||
|
|
||||||
|
public class MinecraftServicesAuthorizer {
|
||||||
|
private static final URL MC_SERVICES_LOGIN = url("https://api.minecraftservices.com/authentication/login_with_xbox");
|
||||||
|
private static final URL MC_SERVICES_PROFILE = url("https://api.minecraftservices.com/minecraft/profile");
|
||||||
|
|
||||||
|
public static McAuthResponse authorizeWithMinecraft(XboxAuthorization auth) throws IOException, InterruptedException {
|
||||||
|
McAuthRequest request = new McAuthRequest("XBL3.0 x=" + auth.getCombinedToken());
|
||||||
|
|
||||||
|
return HttpRequest.post(MC_SERVICES_LOGIN)
|
||||||
|
.bodyJson(request)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.execute()
|
||||||
|
.expectResponseCode(200)
|
||||||
|
.returnContent()
|
||||||
|
.asJson(McAuthResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static McProfileResponse getUserProfile(McAuthResponse auth)
|
||||||
|
throws IOException, InterruptedException, AuthenticationException {
|
||||||
|
HttpRequest request = HttpRequest.get(MC_SERVICES_PROFILE)
|
||||||
|
.header("Authorization", auth.getAuthorization())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (request.getResponseCode() == 200) {
|
||||||
|
return request.returnContent().asJson(McProfileResponse.class);
|
||||||
|
} else {
|
||||||
|
McServicesError error = request.returnContent().asJson(McServicesError.class);
|
||||||
|
|
||||||
|
throw new AuthenticationException(error.getErrorMessage(), error.getErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import lombok.extern.java.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@Log
|
||||||
|
public class OauthHttpHandler {
|
||||||
|
private Executor executor = Executors.newCachedThreadPool();
|
||||||
|
private HttpServer server;
|
||||||
|
private String result;
|
||||||
|
|
||||||
|
public OauthHttpHandler() throws IOException {
|
||||||
|
server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
|
||||||
|
|
||||||
|
server.createContext("/", new Handler());
|
||||||
|
server.setExecutor(executor);
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return server.getAddress().getPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String await() throws InterruptedException {
|
||||||
|
synchronized (this) {
|
||||||
|
this.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
server.stop(3);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Handler implements HttpHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange httpExchange) throws IOException {
|
||||||
|
String query = httpExchange.getRequestURI().getQuery();
|
||||||
|
Map<String, String> qs = Splitter.on('&').withKeyValueSeparator('=').split(query);
|
||||||
|
result = qs.get("code");
|
||||||
|
|
||||||
|
synchronized (OauthHttpHandler.this) {
|
||||||
|
OauthHttpHandler.this.notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] response = "OK: you can close the browser now".getBytes(Charsets.UTF_8);
|
||||||
|
httpExchange.sendResponseHeaders(200, response.length);
|
||||||
|
httpExchange.getResponseBody().write(response);
|
||||||
|
httpExchange.getResponseBody().flush();
|
||||||
|
httpExchange.getResponseBody().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft;
|
||||||
|
|
||||||
|
import com.skcraft.launcher.auth.microsoft.model.*;
|
||||||
|
import com.skcraft.launcher.util.HttpRequest;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||||
|
|
||||||
|
public class XboxTokenAuthorizer {
|
||||||
|
private static final URL XBL_AUTHENTICATE_URL = url("https://user.auth.xboxlive.com/user/authenticate");
|
||||||
|
private static final URL XSTS_AUTHENTICATE_URL = url("https://xsts.auth.xboxlive.com/xsts/authorize");
|
||||||
|
|
||||||
|
public static XboxAuthorization authorizeWithXbox(String accessToken) throws IOException, InterruptedException {
|
||||||
|
XboxAuthRequest<XblAuthProperties> xblPayload =
|
||||||
|
new XboxAuthRequest<>(new XblAuthProperties("d=" + accessToken));
|
||||||
|
|
||||||
|
XboxAuthResponse xblResponse = HttpRequest.post(XBL_AUTHENTICATE_URL)
|
||||||
|
.bodyJson(xblPayload)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.execute()
|
||||||
|
.expectResponseCode(200)
|
||||||
|
.returnContent()
|
||||||
|
.asJson(XboxAuthResponse.class);
|
||||||
|
|
||||||
|
XboxAuthRequest<XstsAuthProperties> xstsPayload =
|
||||||
|
new XboxAuthRequest<>(new XstsAuthProperties(xblResponse.getToken()));
|
||||||
|
xstsPayload.setRelyingParty("rp://api.minecraftservices.com/");
|
||||||
|
|
||||||
|
XboxAuthResponse xstsResponse = HttpRequest.post(XSTS_AUTHENTICATE_URL)
|
||||||
|
.bodyJson(xstsPayload)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.execute()
|
||||||
|
.expectResponseCode(200)
|
||||||
|
.returnContent()
|
||||||
|
.asJson(XboxAuthResponse.class);
|
||||||
|
|
||||||
|
return new XboxAuthorization(xstsResponse.getToken(), xstsResponse.getUhs());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class McAuthRequest {
|
||||||
|
private final String identityToken;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class McAuthResponse {
|
||||||
|
private String accessToken;
|
||||||
|
private String tokenType;
|
||||||
|
private int expiresIn;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public String getAuthorization() {
|
||||||
|
return String.format("%s %s", tokenType, accessToken);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class McProfileResponse {
|
||||||
|
@JsonProperty("id") private String uuid;
|
||||||
|
private String name;
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class McServicesError {
|
||||||
|
private String error;
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class TokenResponse {
|
||||||
|
private String tokenType;
|
||||||
|
private String accessToken;
|
||||||
|
private String refreshToken;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||||
|
public class XblAuthProperties {
|
||||||
|
private String authMethod = "RPS";
|
||||||
|
private String siteName = "user.auth.xboxlive.com";
|
||||||
|
@NonNull
|
||||||
|
private String rpsTicket;
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||||
|
public class XboxAuthRequest<T> {
|
||||||
|
@NonNull private T properties;
|
||||||
|
private String relyingParty = "http://auth.xboxlive.com";
|
||||||
|
private String tokenType = "JWT";
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class XboxAuthResponse {
|
||||||
|
private String token;
|
||||||
|
private DisplayClaims displayClaims;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public String getUhs() {
|
||||||
|
return getDisplayClaims().getXui().get(0).getUhs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class DisplayClaims {
|
||||||
|
private List<UhsContainer> xui;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UhsContainer {
|
||||||
|
private String uhs;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class XboxAuthorization {
|
||||||
|
private final String token;
|
||||||
|
private final String uhs;
|
||||||
|
|
||||||
|
public String getCombinedToken() {
|
||||||
|
return String.format("%s;%s", uhs, token);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.skcraft.launcher.auth.microsoft.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||||
|
public class XstsAuthProperties {
|
||||||
|
private String sandboxId = "RETAIL";
|
||||||
|
private List<String> userTokens;
|
||||||
|
|
||||||
|
public XstsAuthProperties(String token) {
|
||||||
|
this.userTokens = Collections.singletonList(token);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ package com.skcraft.launcher.dialog;
|
|||||||
|
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.skcraft.concurrency.DefaultProgress;
|
||||||
import com.skcraft.concurrency.ObservableFuture;
|
import com.skcraft.concurrency.ObservableFuture;
|
||||||
import com.skcraft.concurrency.ProgressObservable;
|
import com.skcraft.concurrency.ProgressObservable;
|
||||||
import com.skcraft.launcher.Launcher;
|
import com.skcraft.launcher.Launcher;
|
||||||
@ -84,7 +86,7 @@ public class AccountSelectDialog extends JDialog {
|
|||||||
add(listPane, BorderLayout.CENTER);
|
add(listPane, BorderLayout.CENTER);
|
||||||
add(buttonsPanel, BorderLayout.SOUTH);
|
add(buttonsPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
loginButton.addActionListener(ev -> attemptLogin(accountList.getSelectedValue()));
|
loginButton.addActionListener(ev -> attemptExistingLogin(accountList.getSelectedValue()));
|
||||||
cancelButton.addActionListener(ev -> dispose());
|
cancelButton.addActionListener(ev -> dispose());
|
||||||
|
|
||||||
addMojangButton.addActionListener(ev -> {
|
addMojangButton.addActionListener(ev -> {
|
||||||
@ -96,9 +98,7 @@ public class AccountSelectDialog extends JDialog {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addMicrosoftButton.addActionListener(ev -> {
|
addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin());
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
removeSelected.addActionListener(ev -> {
|
removeSelected.addActionListener(ev -> {
|
||||||
if (accountList.getSelectedValue() != null) {
|
if (accountList.getSelectedValue() != null) {
|
||||||
@ -133,7 +133,25 @@ public class AccountSelectDialog extends JDialog {
|
|||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attemptLogin(SavedSession session) {
|
private void attemptMicrosoftLogin() {
|
||||||
|
ListenableFuture<?> future = launcher.getExecutor().submit(() -> {
|
||||||
|
Session newSession = launcher.getMicrosoftLogin().login();
|
||||||
|
|
||||||
|
if (newSession != null) {
|
||||||
|
launcher.getAccounts().add(newSession.toSavedSession());
|
||||||
|
setResult(newSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
String status = SharedLocale.tr("login.loggingInStatus");
|
||||||
|
ProgressDialog.showProgress(this, future, new DefaultProgress(-1, status),
|
||||||
|
SharedLocale.tr("login.loggingInTitle"), status);
|
||||||
|
SwingHelper.addErrorDialogCallback(this, future);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attemptExistingLogin(SavedSession session) {
|
||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
|
|
||||||
LoginService loginService = launcher.getLoginService(session.getType());
|
LoginService loginService = launcher.getLoginService(session.getType());
|
||||||
|
@ -99,15 +99,10 @@ login.playOffline=Play offline
|
|||||||
login.title=Minecraft Login
|
login.title=Minecraft Login
|
||||||
login.idEmail=ID/Email\:
|
login.idEmail=ID/Email\:
|
||||||
login.password=Password\:
|
login.password=Password\:
|
||||||
login.forgetUser=Forget selected user
|
|
||||||
login.forgetPassword=Forget password
|
|
||||||
login.forgetAllPasswords=Forget all passwords...
|
|
||||||
login.confirmForgetAllPasswords=Are you sure that you want to forget all saved passwords?
|
|
||||||
login.forgetAllPasswordsTitle=Forget passwords
|
|
||||||
login.noPasswordError=Please enter a password.
|
login.noPasswordError=Please enter a password.
|
||||||
login.noPasswordTitle=Missing Password
|
login.noPasswordTitle=Missing Password
|
||||||
login.loggingInTitle=Logging in...
|
login.loggingInTitle=Logging in...
|
||||||
login.loggingInStatus=Logging in to Mojang...
|
login.loggingInStatus=Logging in to Minecraft...
|
||||||
login.noLoginError=Please enter your account details.
|
login.noLoginError=Please enter your account details.
|
||||||
login.noLoginTitle=Missing Account
|
login.noLoginTitle=Missing Account
|
||||||
login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account.
|
login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account.
|
||||||
|
@ -13,6 +13,7 @@ versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json
|
|||||||
librariesSource=https://libraries.minecraft.net/
|
librariesSource=https://libraries.minecraft.net/
|
||||||
assetsSource=http://resources.download.minecraft.net/
|
assetsSource=http://resources.download.minecraft.net/
|
||||||
yggdrasilAuthUrl=https://authserver.mojang.com/authenticate
|
yggdrasilAuthUrl=https://authserver.mojang.com/authenticate
|
||||||
|
microsoftClientId=d18bb4d8-a27f-4451-a87f-fe6de4436813
|
||||||
resetPasswordUrl=https://minecraft.net/resetpassword
|
resetPasswordUrl=https://minecraft.net/resetpassword
|
||||||
|
|
||||||
newsUrl=http://update.skcraft.com/template/news.html?version=%s
|
newsUrl=http://update.skcraft.com/template/news.html?version=%s
|
||||||
|
Loading…
Reference in New Issue
Block a user