Microsoft accounts

This commit is contained in:
Bixilon 2021-02-02 20:04:22 +01:00
parent ccccc9cefe
commit 9314b3acb4
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
23 changed files with 451 additions and 47 deletions

View File

@ -122,6 +122,12 @@
<version>${javafx.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>${javafx.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.jfoenix</groupId>
<artifactId>jfoenix</artifactId>

View File

@ -61,6 +61,7 @@ public final class Minosoft {
public static void main(String[] args) {
MinosoftCommandLineArguments.parseCommandLineArguments(args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown(ShutdownReasons.UNKNOWN), "ShutdownHook"));
Util.initUtilClasses();
Log.info("Starting...");
AsyncTaskWorker taskWorker = new AsyncTaskWorker("StartUp");
@ -245,4 +246,5 @@ public final class Minosoft {
public static CountUpAndDownLatch getStartStatusLatch() {
return START_STATUS_LATCH;
}
}

View File

@ -16,6 +16,7 @@ package de.bixilon.minosoft.config;
import com.google.common.collect.HashBiMap;
import com.google.gson.*;
import de.bixilon.minosoft.data.accounts.Account;
import de.bixilon.minosoft.data.accounts.MicrosoftAccount;
import de.bixilon.minosoft.data.accounts.MojangAccount;
import de.bixilon.minosoft.data.accounts.OfflineAccount;
import de.bixilon.minosoft.gui.main.Server;
@ -77,6 +78,7 @@ public class Configuration {
Account account = switch (data.get("type").getAsString()) {
case "mojang" -> MojangAccount.deserialize(data);
case "offline" -> OfflineAccount.deserialize(data);
case "microsoft" -> MicrosoftAccount.deserialize(data);
default -> throw new IllegalArgumentException("Unexpected value: " + data.get("type").getAsString());
};
this.accountList.put(account.getId(), account);
@ -105,16 +107,16 @@ public class Configuration {
JsonObject jsonObject = DEFAULT_CONFIGURATION.deepCopy();
synchronized (this.config) {
// accounts
JsonObject accountsEntriesJson = jsonObject.getAsJsonObject("servers").getAsJsonObject("entries");
// servers
JsonObject serversEntriesJson = jsonObject.getAsJsonObject("servers").getAsJsonObject("entries");
for (Map.Entry<Integer, Server> entry : this.serverList.entrySet()) {
accountsEntriesJson.add(String.valueOf(entry.getKey()), entry.getValue().serialize());
serversEntriesJson.add(String.valueOf(entry.getKey()), entry.getValue().serialize());
}
// servers
JsonObject serversEntriesJson = jsonObject.getAsJsonObject("accounts").getAsJsonObject("entries");
// accounts
JsonObject accountsEntriesJson = jsonObject.getAsJsonObject("accounts").getAsJsonObject("entries");
for (Map.Entry<String, Account> entry : this.accountList.entrySet()) {
serversEntriesJson.add(entry.getKey(), entry.getValue().serialize());
accountsEntriesJson.add(entry.getKey(), entry.getValue().serialize());
}
// rest of data

View File

@ -15,20 +15,34 @@ package de.bixilon.minosoft.data.accounts;
import com.google.gson.JsonObject;
import de.bixilon.minosoft.Minosoft;
import de.bixilon.minosoft.gui.main.cells.AccountListCell;
import de.bixilon.minosoft.util.logging.Log;
import de.bixilon.minosoft.util.mojang.api.exceptions.MojangJoinServerErrorException;
import de.bixilon.minosoft.util.mojang.api.exceptions.NoNetworkConnectionException;
import javafx.application.Platform;
import java.util.UUID;
public abstract class Account {
private final String username;
private final UUID uuid;
protected final String username;
protected final UUID uuid;
protected Account(String username, UUID uuid) {
this.username = username;
this.uuid = uuid;
}
public static void addAccount(Account account) {
Minosoft.getConfig().putAccount(account);
account.saveToConfig();
Log.info(String.format("Added and saved account (type=%s, id=%s, username=%s, uuid=%s)", account.getClass().getSimpleName(), account.getId(), account.getUsername(), account.getUUID()));
Platform.runLater(() -> AccountListCell.ACCOUNT_LIST_VIEW.getItems().add(account));
if (Minosoft.getConfig().getSelectedAccount() == null) {
// select account
Minosoft.selectAccount(account);
}
}
public String getUsername() {
return this.username;
}

View File

@ -0,0 +1,37 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.accounts;
import com.google.gson.JsonObject;
import de.bixilon.minosoft.util.Util;
import java.util.UUID;
public class MicrosoftAccount extends MojangAccount {
public MicrosoftAccount(String accessToken, String id, UUID uuid, String username) {
super(accessToken, id, uuid, username, null);
}
public static MicrosoftAccount deserialize(JsonObject json) {
return new MicrosoftAccount(json.get("accessToken").getAsString(), json.get("id").getAsString(), Util.getUUIDFromString(json.get("uuid").getAsString()), json.get("username").getAsString());
}
public JsonObject serialize() {
JsonObject json = super.serialize();
json.addProperty("type", "microsoft");
json.remove("email");
return json;
}
}

View File

@ -24,11 +24,11 @@ import de.bixilon.minosoft.util.mojang.api.exceptions.NoNetworkConnectionExcepti
import java.util.UUID;
public class MojangAccount extends Account {
private final String id;
private final String email;
private String accessToken;
private RefreshStates lastRefreshStatus;
private boolean needsRefresh = true;
protected final String id;
protected final String email;
protected String accessToken;
protected RefreshStates lastRefreshStatus;
protected boolean needsRefresh = true;
public MojangAccount(String username, JsonObject json) {
super(json.getAsJsonObject("selectedProfile").get("name").getAsString(), Util.getUUIDFromString(json.getAsJsonObject("selectedProfile").get("id").getAsString()));

View File

@ -53,6 +53,7 @@ public enum Strings {
SERVER_ACTION_DELETE,
SESSIONS_ACTION_DISCONNECT,
ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT,
ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT,
ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT,
MAIN_WINDOW_TITLE,
@ -89,6 +90,8 @@ public enum Strings {
LOGIN_OFFLINE_UUID,
LOGIN_OFFLINE_ADD_BUTTON,
LOGIN_MICROSOFT_DIALOG_TITLE,
MINOSOFT_STILL_STARTING_TITLE,
MINOSOFT_STILL_STARTING_HEADER,

View File

@ -34,6 +34,7 @@ public class AccountWindow implements Initializable {
public BorderPane accountPane;
public MenuItem menuAddMojangAccount;
public MenuItem menuAddOfflineAccount;
public MenuItem menuAddMicrosoftAccount;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
@ -43,13 +44,23 @@ public class AccountWindow implements Initializable {
AccountListCell.ACCOUNT_LIST_VIEW.setItems(accounts);
this.accountPane.setCenter(AccountListCell.ACCOUNT_LIST_VIEW);
this.menuAddMicrosoftAccount.setText(LocaleManager.translate(Strings.ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT));
this.menuAddMojangAccount.setText(LocaleManager.translate(Strings.ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT));
this.menuAddOfflineAccount.setText(LocaleManager.translate(Strings.ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT));
}
public void addMicrosoftAccount() {
try {
GUITools.showPane("/layout/dialogs/login/microsoft.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_MICROSOFT_DIALOG_TITLE));
} catch (IOException e) {
e.printStackTrace();
Minosoft.shutdown(e.getMessage(), ShutdownReasons.LAUNCHER_FXML_LOAD_ERROR);
}
}
public void addMojangAccount() {
try {
GUITools.showPane("/layout/dialogs/login_mojang.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_MOJANG_DIALOG_TITLE));
GUITools.showPane("/layout/dialogs/login/mojang.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_MOJANG_DIALOG_TITLE));
} catch (IOException e) {
e.printStackTrace();
Minosoft.shutdown(e.getMessage(), ShutdownReasons.LAUNCHER_FXML_LOAD_ERROR);
@ -58,7 +69,7 @@ public class AccountWindow implements Initializable {
public void addOfflineAccount() {
try {
GUITools.showPane("/layout/dialogs/login_offline.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_OFFLINE_DIALOG_TITLE));
GUITools.showPane("/layout/dialogs/login/offline.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_OFFLINE_DIALOG_TITLE));
} catch (IOException e) {
e.printStackTrace();
Minosoft.shutdown(e.getMessage(), ShutdownReasons.LAUNCHER_FXML_LOAD_ERROR);

View File

@ -0,0 +1,57 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main.dialogs.login;
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.fxml.Initializable;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebView;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.URL;
import java.util.ResourceBundle;
public class MicrosoftLoginController implements Initializable {
public HBox hBox;
public WebView webView;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
CookieHandler.setDefault(new CookieManager());
this.webView.getEngine().setJavaScriptEnabled(true);
this.webView.setContextMenuEnabled(false);
this.webView.getEngine().loadContent("Loading...");
this.webView.getEngine().getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
if (this.webView.getEngine().getLocation().startsWith("ms-xal-" + ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID)) {
// login is being handled by MicrosoftOAuthUtils. We can go now...
this.hBox.getScene().getWindow().hide();
}
}
});
requestOathFlowToken();
}
private void requestOathFlowToken() {
this.webView.getEngine().load(ProtocolDefinition.MICROSOFT_ACCOUNT_OAUTH_FLOW_URL);
}
public void login(ActionEvent event) {
event.consume();
}
}

View File

@ -11,17 +11,15 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main.dialogs;
package de.bixilon.minosoft.gui.main.dialogs.login;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXTextField;
import de.bixilon.minosoft.Minosoft;
import de.bixilon.minosoft.data.accounts.Account;
import de.bixilon.minosoft.data.accounts.MojangAccount;
import de.bixilon.minosoft.data.locale.LocaleManager;
import de.bixilon.minosoft.data.locale.Strings;
import de.bixilon.minosoft.gui.main.cells.AccountListCell;
import de.bixilon.minosoft.util.logging.Log;
import de.bixilon.minosoft.util.mojang.api.MojangAuthentication;
import de.bixilon.minosoft.util.mojang.api.exceptions.AuthenticationException;
import de.bixilon.minosoft.util.mojang.api.exceptions.NoNetworkConnectionException;
@ -85,19 +83,10 @@ public class MojangLoginController implements Initializable {
new Thread(() -> { // ToDo: recycle thread
try {
MojangAccount account = MojangAuthentication.login(this.email.getText(), this.password.getText());
Account.addAccount(account);
Platform.runLater(this::close);
account.setNeedRefresh(false);
Minosoft.getConfig().putAccount(account);
account.saveToConfig();
Log.info(String.format("Added and saved account (type=mojang, username=%s, email=%s, uuid=%s)", account.getUsername(), account.getEmail(), account.getUUID()));
Platform.runLater(() -> {
AccountListCell.ACCOUNT_LIST_VIEW.getItems().add(account);
close();
});
if (Minosoft.getConfig().getSelectedAccount() == null) {
// select account
Minosoft.selectAccount(account);
}
} catch (AuthenticationException | NoNetworkConnectionException e) {
e.printStackTrace();
Platform.runLater(() -> {

View File

@ -11,7 +11,7 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main.dialogs;
package de.bixilon.minosoft.gui.main.dialogs.login;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;

View File

@ -13,12 +13,12 @@
package de.bixilon.minosoft.protocol.protocol;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import de.bixilon.minosoft.data.inventory.Slot;
import de.bixilon.minosoft.data.text.ChatComponent;
import de.bixilon.minosoft.data.world.BlockPosition;
import de.bixilon.minosoft.protocol.network.Connection;
import de.bixilon.minosoft.util.Util;
import de.bixilon.minosoft.util.nbt.tag.CompoundTag;
import java.nio.charset.StandardCharsets;
@ -82,7 +82,7 @@ public class OutByteBuffer {
}
public void writeJSON(JsonObject json) {
writeString(new Gson().toJson(json));
writeString(Util.GSON.toJson(json));
}
public void writeString(String string) {

View File

@ -71,6 +71,15 @@ public final class ProtocolDefinition {
public static final String MOJANG_URL_JOIN = "https://sessionserver.mojang.com/session/minecraft/join";
public static final String MOJANG_URL_REFRESH = "https://authserver.mojang.com/refresh";
public static final String MICROSOFT_ACCOUNT_APPLICATION_ID = "00000000402b5328"; // ToDo: Should we use our own application id?
// public static final String MICROSOFT_ACCOUNT_APPLICATION_ID = "fe6f0fbf-3038-486a-9c84-6a28b71e0455";
public static final String MICROSOFT_ACCOUNT_OAUTH_FLOW_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=" + MICROSOFT_ACCOUNT_APPLICATION_ID + "&scope=XboxLive.signin%20offline_access&response_type=code";
public static final String MICROSOFT_ACCOUNT_AUTH_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
public static final String MICROSOFT_ACCOUNT_XBOX_LIVE_AUTHENTICATE_URL = "https://user.auth.xboxlive.com/user/authenticate";
public static final String MICROSOFT_ACCOUNT_XSTS_URL = "https://xsts.auth.xboxlive.com/xsts/authorize";
public static final String MICROSOFT_ACCOUNT_MINECRAFT_LOGIN_WITH_XBOX_URL = "https://api.minecraftservices.com/authentication/login_with_xbox";
public static final String MICROSOFT_ACCOUNT_GET_MOJANG_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile";
public static final char[] OBFUSCATED_CHARS = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".toCharArray();
static {

View File

@ -22,26 +22,64 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
public final class HTTP {
public static HttpResponse<String> postJson(String url, JsonObject json) throws IOException, InterruptedException {
public static HttpResponse<String> postJson(String url, String json, HashMap<String, String> headers) throws IOException, InterruptedException {
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.ofString(json.toString())).header("Content-Type", "application/json").build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(json))
.headers(Util.headersMapToArray(headers))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
public static HttpResponse<String> postJson(String url, JsonObject json) throws IOException, InterruptedException {
return postJson(url, Util.GSON.toJson(json), new HashMap<>());
}
public static HttpResponse<String> postJson(String url, String json) throws IOException, InterruptedException {
return postJson(url, json, new HashMap<>());
}
public static HttpResponse<String> get(String url, HashMap<String, String> headers) throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.headers(Util.headersMapToArray(headers))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
public static HttpResponse<String> get(String url) throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
return get(url, new HashMap<>());
}
public static JsonElement getJson(String url) throws IOException, InterruptedException {
HttpResponse<String> response = get(url);
public static JsonElement getJson(String url, HashMap<String, String> headers) throws IOException, InterruptedException {
HttpResponse<String> response = get(url, headers);
if (response.statusCode() != 200) {
throw new IOException();
}
return JsonParser.parseString(response.body());
}
public static JsonElement getJson(String url) throws IOException, InterruptedException {
return getJson(url, new HashMap<>());
}
public static HttpResponse<String> postData(String url, HashMap<String, String> data) throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(Util.mapToUrlQuery(data)))
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
}

View File

@ -14,24 +14,26 @@
package de.bixilon.minosoft.util;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import de.bixilon.minosoft.protocol.network.Connection;
import de.bixilon.minosoft.protocol.protocol.InByteBuffer;
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition;
import de.bixilon.minosoft.util.logging.Log;
import de.bixilon.minosoft.util.microsoft.MicrosoftOAuthUtils;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Random;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
@ -46,6 +48,8 @@ public final class Util {
private static final Field JSON_READER_POS_FIELD;
private static final Field JSON_READER_LINE_START_FIELD;
public static final Gson GSON = new Gson();
static {
new JsonReader(new StringReader(""));
Class<?> jsonReadClass = JsonReader.class;
@ -324,4 +328,48 @@ public final class Util {
throw new IllegalArgumentException("Not a valid url:" + url);
}
}
public static <T> void forceClassInit(Class<T> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException exception) {
throw new RuntimeException(exception);
}
}
public static void initUtilClasses() {
forceClassInit(Log.class);
forceClassInit(MicrosoftOAuthUtils.class);
}
public static Map<String, String> urlQueryToMap(String query) {
Map<String, String> map = new HashMap<>();
for (String parameter : query.split("&")) {
String[] split = parameter.split("=");
map.put(split[0], split[1]);
}
return map;
}
public static String mapToUrlQuery(Map<String, String> data) {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : data.entrySet()) {
if (!builder.isEmpty()) {
builder.append("&");
}
builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
builder.append("=");
builder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
}
return builder.toString();
}
public static String[] headersMapToArray(Map<String, String> headers) {
List<String> headerList = new ArrayList<>();
for (var entry : headers.entrySet()) {
headerList.add(entry.getKey());
headerList.add(entry.getValue());
}
return headerList.toArray(new String[]{});
}
}

View File

@ -0,0 +1,3 @@
package de.bixilon.minosoft.util.microsoft
class LoginException(val errorCode: Int, override val message: String, val errorMessage: String) : Exception()

View File

@ -0,0 +1,173 @@
package de.bixilon.minosoft.util.microsoft
import com.google.gson.JsonParser
import com.jfoenix.controls.JFXAlert
import com.jfoenix.controls.JFXDialogLayout
import de.bixilon.minosoft.config.StaticConfiguration
import de.bixilon.minosoft.data.accounts.Account
import de.bixilon.minosoft.data.accounts.MicrosoftAccount
import de.bixilon.minosoft.gui.main.GUITools
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.util.HTTP
import de.bixilon.minosoft.util.Util
import de.bixilon.minosoft.util.logging.Log
import javafx.application.Platform
import javafx.scene.control.TextArea
import javafx.scene.text.Text
import javafx.stage.Stage
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
object MicrosoftOAuthUtils {
val NULL_URL_CONNECTION: URLConnection = object : URLConnection(null) {
override fun connect() {}
}
fun loginToMicrosoftAccount(authorizationCode: String) {
Log.verbose("Logging into microsoft account...")
try {
val authorizationToken = getAuthorizationToken(authorizationCode)
val xboxLiveToken = getXboxLiveToken(authorizationToken)
val xstsToken = getXSTSToken(xboxLiveToken.first)
val microsoftAccount = getMicrosoftAccount(getMinecraftAccessToken(xboxLiveToken.second, xstsToken))
Account.addAccount(microsoftAccount)
} catch (exception: Exception) {
Log.warn("Can not login into microsoft account")
exception.printStackTrace()
if (!StaticConfiguration.HEADLESS_MODE) {
var message = "Could not login!"
var errorMessage = exception.javaClass.canonicalName + ": " + exception.message
if (exception is LoginException) {
message = "${exception.message} (${exception.errorCode})"
errorMessage = exception.errorMessage
}
Platform.runLater {
val dialog = JFXAlert<Boolean>()
GUITools.initializePane(dialog.dialogPane)
// Do not translate this, translations might fail to load...
dialog.title = "Login error"
val layout = JFXDialogLayout()
layout.setHeading(Text(message))
val text = TextArea(errorMessage)
text.isEditable = false
text.isWrapText = true
layout.setBody(text)
dialog.dialogPane.content = layout
val stage = dialog.dialogPane.scene.window as Stage
stage.toFront()
dialog.show()
}
}
}
}
fun getAuthorizationToken(authorizationCode: String): String {
val data = mapOf(
"client_id" to ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID,
"code" to authorizationCode,
"grant_type" to "authorization_code",
"scope" to "service::user.auth.xboxlive.com::MBI_SSL",
)
val response = HTTP.postData(ProtocolDefinition.MICROSOFT_ACCOUNT_AUTH_TOKEN_URL, HashMap(data))
if (response.statusCode() != 200) {
throw LoginException(response.statusCode(), "Could not get authorization token ", response.body())
}
val body = JsonParser.parseString(response.body()).asJsonObject
return body["access_token"]!!.asString
}
/**
* returns A: XBL Token; B: UHS Token
*/
fun getXboxLiveToken(authorizationToken: String): Pair<String, String> {
val payload = mapOf(
"Properties" to mapOf(
"AuthMethod" to "RPS",
"SiteName" to "user.auth.xboxlive.com",
"RpsTicket" to authorizationToken
),
"RelyingParty" to "http://auth.xboxlive.com",
"TokenType" to "JWT",
)
val response = HTTP.postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_XBOX_LIVE_AUTHENTICATE_URL, Util.GSON.toJson(payload))
if (response.statusCode() != 200) {
throw LoginException(response.statusCode(), "Could not get authenticate against xbox live ", response.body())
}
val body = JsonParser.parseString(response.body()).asJsonObject
return Pair(body["Token"]!!.asString, body["DisplayClaims"].asJsonObject["xui"].asJsonArray[0].asJsonObject["uhs"].asString)
}
fun getXSTSToken(xBoxLiveToken: String): String {
val payload = mapOf(
"Properties" to mapOf(
"SandboxId" to "RETAIL",
"UserTokens" to listOf(xBoxLiveToken)
),
"RelyingParty" to "rp://api.minecraftservices.com/",
"TokenType" to "JWT",
)
val response = HTTP.postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_XSTS_URL, Util.GSON.toJson(payload))
if (response.statusCode() != 200) {
val error = JsonParser.parseString(response.body()).asJsonObject
val errorMessage = when (error["XErr"].asLong) {
2148916233 -> "You don't have an XBox account!"
2148916238 -> "This account is a child account!"
else -> error["Message"].asString
}
throw LoginException(response.statusCode(), "Could not get authenticate against XSTS token ", errorMessage)
}
val body = JsonParser.parseString(response.body()).asJsonObject
return body["Token"].asString!!
}
fun getMinecraftAccessToken(uhs: String, xstsToken: String): String {
val payload = mapOf(
"identityToken" to "XBL3.0 x=${uhs};${xstsToken}"
)
val response = HTTP.postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_MINECRAFT_LOGIN_WITH_XBOX_URL, Util.GSON.toJson(payload))
if (response.statusCode() != 200) {
val error = JsonParser.parseString(response.body()).asJsonObject
throw LoginException(response.statusCode(), "Could not get minecraft access token ", error["errorMessage"].asString)
}
val body = JsonParser.parseString(response.body()).asJsonObject
return body["access_token"].asString!!
}
fun getMicrosoftAccount(bearerToken: String): MicrosoftAccount {
val response = HTTP.get(ProtocolDefinition.MICROSOFT_ACCOUNT_GET_MOJANG_PROFILE_URL, HashMap(mapOf(
"Authorization" to "Bearer $bearerToken"
)))
if (response.statusCode() != 200) {
val errorMessage = when (response.statusCode()) {
404 -> "You don't have a copy of minecraft!"
else -> JsonParser.parseString(response.body()).asJsonObject["errorMessage"].asString
}
throw LoginException(response.statusCode(), "Could not get minecraft profile", errorMessage)
}
val body = JsonParser.parseString(response.body()).asJsonObject
return MicrosoftAccount(bearerToken, body["id"].asString!!, Util.getUUIDFromString(body["id"].asString!!), body["name"].asString!!)
}
init {
URL.setURLStreamHandlerFactory {
if (it == "ms-xal-" + ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID) {
return@setURLStreamHandlerFactory object : URLStreamHandler() {
override fun openConnection(url: URL): URLConnection {
loginToMicrosoftAccount(Util.urlQueryToMap(url.query)["code"]!!)
return NULL_URL_CONNECTION
}
}
}
return@setURLStreamHandlerFactory null
}
}
}

View File

@ -36,6 +36,7 @@
"SERVER_ACTION_SESSIONS": "Verbindungen",
"SERVER_ACTION_DELETE": "Löschen",
"SESSIONS_ACTION_DISCONNECT": "Trennen",
"ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT": "Anmelden (Microsoft)",
"ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT": "Anmelden (Mojang)",
"ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT": "Hinzufügen (Offline)",
"MAIN_WINDOW_TITLE": "Minosoft",
@ -63,6 +64,7 @@
"LOGIN_OFFLINE_USERNAME": "Benutzername",
"LOGIN_OFFLINE_UUID": "(UUID)",
"LOGIN_OFFLINE_ADD_BUTTON": "Hinzufügen",
"LOGIN_MICROSOFT_DIALOG_TITLE": "Anmelden (Microsoft) - Mojang",
"ERROR": "Fehler",
"MINOSOFT_STILL_STARTING_TITLE": "Bitte warten",
"MINOSOFT_STILL_STARTING_HEADER": "Minosoft muss noch ein paar Dinge erledigen, bevor du es verwenden kannst.\nBitte warte noch ein paar Sekunden...",

View File

@ -37,6 +37,7 @@
"SERVER_ACTION_SESSIONS": "Sessions",
"SERVER_ACTION_DELETE": "Delete",
"SESSIONS_ACTION_DISCONNECT": "Disconnect",
"ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT": "Login (Microsoft)",
"ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT": "Login (Mojang)",
"ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT": "Add (Offline)",
"MAIN_WINDOW_TITLE": "Minosoft",
@ -64,6 +65,7 @@
"LOGIN_OFFLINE_USERNAME": "Username",
"LOGIN_OFFLINE_UUID": "(UUID)",
"LOGIN_OFFLINE_ADD_BUTTON": "Add",
"LOGIN_MICROSOFT_DIALOG_TITLE": "Login (Microsoft) - Minosoft",
"ERROR": "Error",
"MINOSOFT_STILL_STARTING_TITLE": "Please wait",
"MINOSOFT_STILL_STARTING_HEADER": "Minosoft is still starting up...",

View File

@ -16,8 +16,9 @@
<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" prefHeight="400.0" prefWidth="640.0" fx:controller="de.bixilon.minosoft.gui.main.AccountWindow">
<MenuBar VBox.vgrow="NEVER">
<Menu mnemonicParsing="false" text="_Accounts">
<MenuItem fx:id="menuAddMojangAccount" mnemonicParsing="false" onAction="#addMojangAccount" text="-Add Mojang account-"/>
<MenuItem fx:id="menuAddMicrosoftAccount" mnemonicParsing="false" onAction="#addMicrosoftAccount" text="-Add Microsoft account-"/>
<MenuItem fx:id="menuAddOfflineAccount" mnemonicParsing="false" onAction="#addOfflineAccount" text="-Add Offline account-"/>
<MenuItem fx:id="menuAddMojangAccount" mnemonicParsing="false" onAction="#addMojangAccount" text="-Add Mojang account-"/>
</Menu>
</MenuBar>
<AnchorPane VBox.vgrow="ALWAYS">

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.web.WebView?>
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="800.0" prefWidth="700.0" xmlns="http://javafx.com/javafx/15.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.login.MicrosoftLoginController">
<WebView fx:id="webView" minHeight="100.0" minWidth="100.0" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="ALWAYS"/>
</HBox>

View File

@ -7,7 +7,7 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.MojangLoginController">
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.login.MojangLoginController">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>

View File

@ -6,7 +6,7 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.OfflineLoginController">
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.login.OfflineLoginController">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>