diff --git a/launcher/src/main/java/com/skcraft/launcher/Launcher.java b/launcher/src/main/java/com/skcraft/launcher/Launcher.java index 10ca1df..21b44d4 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Launcher.java +++ b/launcher/src/main/java/com/skcraft/launcher/Launcher.java @@ -12,10 +12,7 @@ import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import com.skcraft.launcher.auth.AccountList; -import com.skcraft.launcher.auth.LoginService; -import com.skcraft.launcher.auth.UserType; -import com.skcraft.launcher.auth.YggdrasilLoginService; +import com.skcraft.launcher.auth.*; import com.skcraft.launcher.launch.LaunchSupervisor; import com.skcraft.launcher.model.minecraft.Library; 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()); } + public MicrosoftLoginService getMicrosoftLogin() { + return new MicrosoftLoginService(getProperties().getProperty("microsoftClientId")); + } + public LoginService getLoginService(UserType type) { if (type == UserType.MICROSOFT) { return null; // TODO: Microsoft login service diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java new file mode 100644 index 0000000..3d418fc --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java @@ -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 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 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; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java b/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java index 4570ae9..1e24b3b 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java @@ -37,11 +37,6 @@ public class OfflineSession implements Session { return (new UUID(0, 0)).toString(); } - @Override - public String getClientToken() { - return "0"; - } - @Override public String getAccessToken() { return "0"; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/Session.java b/launcher/src/main/java/com/skcraft/launcher/auth/Session.java index 326dd93..51a2841 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/Session.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/Session.java @@ -27,13 +27,6 @@ public interface Session { */ String getName(); - /** - * Get the client token. - * - * @return client token - */ - String getClientToken(); - /** * Get the access token. * diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java index 35911b8..9faa157 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java @@ -124,12 +124,6 @@ public class YggdrasilLoginService implements LoginService { return String.format("token:%s:%s", getAccessToken(), getUuid()); } - @Override - @JsonIgnore - public String getClientToken() { - return response.getClientToken(); - } - @Override @JsonIgnore public String getAccessToken() { diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java new file mode 100644 index 0000000..5d46e91 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java @@ -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; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java new file mode 100644 index 0000000..4fb648d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java @@ -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()); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java new file mode 100644 index 0000000..59b0acf --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java @@ -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 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(); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/XboxTokenAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/XboxTokenAuthorizer.java new file mode 100644 index 0000000..6230391 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/XboxTokenAuthorizer.java @@ -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 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 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()); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthRequest.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthRequest.java new file mode 100644 index 0000000..505da69 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthRequest.java @@ -0,0 +1,8 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import lombok.Data; + +@Data +public class McAuthRequest { + private final String identityToken; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java new file mode 100644 index 0000000..768c05f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java @@ -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); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java new file mode 100644 index 0000000..7c22645 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java @@ -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; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McServicesError.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McServicesError.java new file mode 100644 index 0000000..034f05d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McServicesError.java @@ -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; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java new file mode 100644 index 0000000..d6aeb60 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java @@ -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; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java new file mode 100644 index 0000000..1414687 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java @@ -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; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java new file mode 100644 index 0000000..53c4498 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java @@ -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 { + @NonNull private T properties; + private String relyingParty = "http://auth.xboxlive.com"; + private String tokenType = "JWT"; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java new file mode 100644 index 0000000..c7893a8 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java @@ -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 xui; + } + + @Data + public static class UhsContainer { + private String uhs; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthorization.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthorization.java new file mode 100644 index 0000000..499a0e6 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthorization.java @@ -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); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java new file mode 100644 index 0000000..63fcef8 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java @@ -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 userTokens; + + public XstsAuthProperties(String token) { + this.userTokens = Collections.singletonList(token); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java index a511882..8a07a91 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java @@ -2,6 +2,8 @@ package com.skcraft.launcher.dialog; import com.google.common.util.concurrent.FutureCallback; 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.ProgressObservable; import com.skcraft.launcher.Launcher; @@ -84,7 +86,7 @@ public class AccountSelectDialog extends JDialog { add(listPane, BorderLayout.CENTER); add(buttonsPanel, BorderLayout.SOUTH); - loginButton.addActionListener(ev -> attemptLogin(accountList.getSelectedValue())); + loginButton.addActionListener(ev -> attemptExistingLogin(accountList.getSelectedValue())); cancelButton.addActionListener(ev -> dispose()); addMojangButton.addActionListener(ev -> { @@ -96,9 +98,7 @@ public class AccountSelectDialog extends JDialog { } }); - addMicrosoftButton.addActionListener(ev -> { - // TODO - }); + addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin()); removeSelected.addActionListener(ev -> { if (accountList.getSelectedValue() != null) { @@ -133,7 +133,25 @@ public class AccountSelectDialog extends JDialog { 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; LoginService loginService = launcher.getLoginService(session.getType()); diff --git a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index e673794..c58e082 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -99,15 +99,10 @@ login.playOffline=Play offline login.title=Minecraft Login login.idEmail=ID/Email\: 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.noPasswordTitle=Missing Password login.loggingInTitle=Logging in... -login.loggingInStatus=Logging in to Mojang... +login.loggingInStatus=Logging in to Minecraft... login.noLoginError=Please enter your account details. login.noLoginTitle=Missing Account login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account. diff --git a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties index 67119ca..3d9e202 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties @@ -13,6 +13,7 @@ versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json librariesSource=https://libraries.minecraft.net/ assetsSource=http://resources.download.minecraft.net/ yggdrasilAuthUrl=https://authserver.mojang.com/authenticate +microsoftClientId=d18bb4d8-a27f-4451-a87f-fe6de4436813 resetPasswordUrl=https://minecraft.net/resetpassword newsUrl=http://update.skcraft.com/template/news.html?version=%s