diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/Account.kt b/src/main/java/de/bixilon/minosoft/data/accounts/Account.kt index cf6c678ba..9ce3b4f1a 100644 --- a/src/main/java/de/bixilon/minosoft/data/accounts/Account.kt +++ b/src/main/java/de/bixilon/minosoft/data/accounts/Account.kt @@ -63,4 +63,8 @@ abstract class Account( } check(latch, clientToken) } + + fun save() { + println("ToDo") // ToDo + } } diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftAccount.kt b/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftAccount.kt index c1096823e..18a30627a 100644 --- a/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftAccount.kt +++ b/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftAccount.kt @@ -13,9 +13,9 @@ package de.bixilon.minosoft.data.accounts.types.microsoft -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import de.bixilon.kutil.latch.CountUpAndDownLatch +import de.bixilon.kutil.time.TimeUtil import de.bixilon.minosoft.data.accounts.Account import de.bixilon.minosoft.data.accounts.AccountStates import de.bixilon.minosoft.data.player.properties.PlayerProperties @@ -30,34 +30,27 @@ import java.util.* class MicrosoftAccount( val uuid: UUID, username: String, - @field:JsonProperty private val authorizationToken: String, + @field:JsonProperty private var msa: MicrosoftTokens, + @field:JsonProperty private var minecraft: MinecraftTokens, override val properties: PlayerProperties?, ) : Account(username) { - @Transient @JsonIgnore var accessToken: String? = null override val id: String = uuid.toString() override val type: ResourceLocation = RESOURCE_LOCATION @Synchronized override fun join(serverId: String) { - AccountUtil.joinMojangServer(username, accessToken!!, uuid, serverId) + tryCheck(null, "null") + AccountUtil.joinMojangServer(username, minecraft.accessToken, uuid, serverId) } override fun logout(clientToken: String) = Unit @Synchronized override fun check(latch: CountUpAndDownLatch?, @Nullable clientToken: String) { - if (accessToken != null) { - return - } - val innerLatch = CountUpAndDownLatch(3, latch) + val innerLatch = CountUpAndDownLatch(1, latch) try { - state = AccountStates.REFRESHING - val (xboxLiveToken, userHash) = MicrosoftOAuthUtils.getXboxLiveToken(authorizationToken) - innerLatch.dec() - val xstsToken = MicrosoftOAuthUtils.getXSTSToken(xboxLiveToken) - innerLatch.dec() - - accessToken = MicrosoftOAuthUtils.getMinecraftBearerAccessToken(userHash, xstsToken) + this.error = null + checkMinecraftToken(innerLatch) innerLatch.dec() state = AccountStates.WORKING } catch (exception: Throwable) { @@ -68,6 +61,71 @@ class MicrosoftAccount( } } + override fun tryCheck(latch: CountUpAndDownLatch?, clientToken: String) { + if (state == AccountStates.CHECKING || state == AccountStates.REFRESHING) { + // already checking + return + } + if (minecraft.expires >= TimeUtil.time / 1000) { + return check(latch, "null") + } + if (state == AccountStates.WORKING) { + // Nothing to do + return + } + check(latch, clientToken) + } + + private fun refreshMicrosoftToken(latch: CountUpAndDownLatch?) { + state = AccountStates.REFRESHING + latch?.inc() + msa = MicrosoftOAuthUtils.refreshToken(msa).saveTokens() + latch?.dec() + } + + private fun refreshMinecraftToken(latch: CountUpAndDownLatch?) { + state = AccountStates.REFRESHING + val time = TimeUtil.time / 1000 + if (time >= msa.expires) { + // token expired + refreshMicrosoftToken(latch) + } + + try { + latch?.let { it.count += 3 } + val xboxLiveToken = MicrosoftOAuthUtils.getXboxLiveToken(msa) + latch?.dec() + val xstsToken = MicrosoftOAuthUtils.getXSTSToken(xboxLiveToken) + latch?.dec() + + minecraft = MicrosoftOAuthUtils.getMinecraftBearerAccessToken(xboxLiveToken, xstsToken).saveTokens() + latch?.dec() + } catch (exception: Throwable) { + exception.printStackTrace() + refreshMicrosoftToken(latch) + } + save() + } + + private fun checkMinecraftToken(latch: CountUpAndDownLatch?) { + state = AccountStates.CHECKING + val time = TimeUtil.time / 1000 + if (time >= minecraft.expires) { + // token expired + refreshMinecraftToken(latch) + } + + try { + latch?.inc() + AccountUtil.fetchMinecraftProfile(minecraft) + latch?.dec() + state = AccountStates.WORKING + } catch (exception: Throwable) { + exception.printStackTrace() + refreshMinecraftToken(latch) + } + } + override fun toString(): String { return "MicrosoftAccount{$username}" } diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftTokens.kt b/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftTokens.kt new file mode 100644 index 000000000..0a27decba --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MicrosoftTokens.kt @@ -0,0 +1,20 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.data.accounts.types.microsoft + +data class MicrosoftTokens( + val accessToken: String, + val refreshToken: String, + val expires: Long, +) diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/MojangAccountInfo.kt b/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MinecraftTokens.kt similarity index 80% rename from src/main/java/de/bixilon/minosoft/data/accounts/MojangAccountInfo.kt rename to src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MinecraftTokens.kt index a49002843..434bf36a0 100644 --- a/src/main/java/de/bixilon/minosoft/data/accounts/MojangAccountInfo.kt +++ b/src/main/java/de/bixilon/minosoft/data/accounts/types/microsoft/MinecraftTokens.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2021 Moritz Zwerger + * Copyright (C) 2020-2022 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. * @@ -11,10 +11,9 @@ * This software is not affiliated with Mojang AB, the original developer of Minecraft. */ -package de.bixilon.minosoft.data.accounts +package de.bixilon.minosoft.data.accounts.types.microsoft -data class MojangAccountInfo( - val id: String, - val name: String, - // ToDo: Skins, Capes +data class MinecraftTokens( + val accessToken: String, + val expires: Long, ) diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/types/mojang/MojangAccount.kt b/src/main/java/de/bixilon/minosoft/data/accounts/types/mojang/MojangAccount.kt index 06e5ca42a..fc93a8ddb 100644 --- a/src/main/java/de/bixilon/minosoft/data/accounts/types/mojang/MojangAccount.kt +++ b/src/main/java/de/bixilon/minosoft/data/accounts/types/mojang/MojangAccount.kt @@ -97,6 +97,7 @@ class MojangAccount( refreshed = true state = AccountStates.WORKING + save() Log.log(LogMessageType.AUTHENTICATION, LogLevels.VERBOSE) { "Mojang account refresh successful (username=$username)" } } diff --git a/src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt b/src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt index 8590a4770..d5d163f3a 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2021 Moritz Zwerger + * Copyright (C) 2020-2022 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. * @@ -16,10 +16,10 @@ package de.bixilon.minosoft.data.text import de.bixilon.kutil.enums.EnumUtil import de.bixilon.kutil.enums.ValuesEnum -enum class URLProtocols(val prefix: String, val restricted: Boolean = false) { - HTTP("http://"), - HTTPS("https://"), - FILE("file:", true), +enum class URLProtocols(val protocol: String, val prefix: String, val restricted: Boolean = false) { + HTTP("http", "http://"), + HTTPS("https", "https://"), + FILE("file", "file:", true), ; companion object : ValuesEnum { diff --git a/src/main/java/de/bixilon/minosoft/gui/eros/main/account/AccountController.kt b/src/main/java/de/bixilon/minosoft/gui/eros/main/account/AccountController.kt index a12cd7b1f..46dcb02ff 100644 --- a/src/main/java/de/bixilon/minosoft/gui/eros/main/account/AccountController.kt +++ b/src/main/java/de/bixilon/minosoft/gui/eros/main/account/AccountController.kt @@ -253,8 +253,8 @@ class AccountController : EmbeddedJavaFXController() { "minosoft:main.account.account_info.uuid".toResourceLocation() to { it.uuid }, ), icon = FontAwesomeBrands.MICROSOFT, - addHandler = { MicrosoftAddController(it).show() }, - refreshHandler = { controller, account -> MicrosoftAddController(controller, account).show() } + addHandler = { MicrosoftAddController(it).request() }, + refreshHandler = { controller, account -> MicrosoftAddController(controller, account).request() } ), ErosAccountType( resourceLocation = OfflineAccount.RESOURCE_LOCATION, diff --git a/src/main/java/de/bixilon/minosoft/gui/eros/main/account/add/MicrosoftAddController.kt b/src/main/java/de/bixilon/minosoft/gui/eros/main/account/add/MicrosoftAddController.kt index 85c2018b6..cfae558f4 100644 --- a/src/main/java/de/bixilon/minosoft/gui/eros/main/account/add/MicrosoftAddController.kt +++ b/src/main/java/de/bixilon/minosoft/gui/eros/main/account/add/MicrosoftAddController.kt @@ -13,41 +13,81 @@ package de.bixilon.minosoft.gui.eros.main.account.add +import de.bixilon.minosoft.Minosoft import de.bixilon.minosoft.config.profile.profiles.eros.ErosProfileManager import de.bixilon.minosoft.data.accounts.types.microsoft.MicrosoftAccount import de.bixilon.minosoft.gui.eros.controller.JavaFXWindowController +import de.bixilon.minosoft.gui.eros.dialog.ErosErrorReport.Companion.report import de.bixilon.minosoft.gui.eros.main.account.AccountController import de.bixilon.minosoft.gui.eros.util.JavaFXUtil +import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.ctext +import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.text import de.bixilon.minosoft.util.KUtil.toResourceLocation +import de.bixilon.minosoft.util.account.microsoft.AuthenticationResponse +import de.bixilon.minosoft.util.account.microsoft.MicrosoftOAuthUtils +import de.bixilon.minosoft.util.account.microsoft.code.MicrosoftDeviceCode import javafx.fxml.FXML +import javafx.scene.control.Button import javafx.scene.control.TextField import javafx.scene.text.TextFlow import javafx.stage.Modality +import java.net.URL class MicrosoftAddController( private val accountController: AccountController, private val account: MicrosoftAccount? = null, ) : JavaFXWindowController() { - @FXML private lateinit var textFX: TextFlow + private val profile = ErosProfileManager.selected.general.accountProfile + @FXML private lateinit var headerFX: TextFlow @FXML private lateinit var codeFX: TextField + @FXML private lateinit var cancelFX: Button - fun show() { + fun request() { + MicrosoftOAuthUtils.obtainDeviceCodeAsync(this::codeCallback, this::errorCallback, this::authenticationResponseCallback) + } + + private fun errorCallback(exception: Throwable) { + JavaFXUtil.runLater { stage.close() } + exception.report() + } + + private fun codeCallback(code: MicrosoftDeviceCode) { JavaFXUtil.runLater { JavaFXUtil.openModal(TITLE, LAYOUT, this, modality = Modality.APPLICATION_MODAL) + headerFX.text = HEADER(code.verificationURI) + codeFX.text = code.userCode stage.show() } } + private fun authenticationResponseCallback(response: AuthenticationResponse) { + val account = MicrosoftOAuthUtils.loginToMicrosoftAccount(response) + profile.entries[account.id] = account + if (this.account == null) { + profile.selected = account + } + JavaFXUtil.runLater { + stage.hide() + accountController.refreshList() + } + } + override fun init() { super.init() - val profile = ErosProfileManager.selected.general.accountProfile + cancelFX.ctext = CANCEL + } + @FXML + fun cancel() { + TODO() } companion object { private val LAYOUT = "minosoft:eros/main/account/add/microsoft.fxml".toResourceLocation() private val TITLE = "minosoft:main.account.add.microsoft.title".toResourceLocation() + private val HEADER = { link: URL -> Minosoft.LANGUAGE_MANAGER.translate("minosoft:main.account.add.microsoft.header".toResourceLocation(), null, link) } + private val CANCEL = "minosoft:main.account.add.microsoft.cancel".toResourceLocation() } } diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java index dc5baffa4..76e2eca56 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020 Moritz Zwerger + * Copyright (C) 2020-2022 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. * @@ -66,8 +66,8 @@ public final class ProtocolDefinition { public static final int ITEM_STACK_MAX_SIZE = 64; - 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_APPLICATION_ID = "00000000402b5328"; // ToDo: Should we use our own application id? + public static final String MICROSOFT_ACCOUNT_APPLICATION_ID = "feb3836f-0333-4185-8eb9-4cbf0498f947"; // Minosoft 2 (microsoft-bixilon2) 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"; diff --git a/src/main/java/de/bixilon/minosoft/util/account/AccountUtil.kt b/src/main/java/de/bixilon/minosoft/util/account/AccountUtil.kt index 7e55efb64..7f916c912 100644 --- a/src/main/java/de/bixilon/minosoft/util/account/AccountUtil.kt +++ b/src/main/java/de/bixilon/minosoft/util/account/AccountUtil.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2021 Moritz Zwerger + * Copyright (C) 2020-2022 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. * @@ -13,14 +13,14 @@ package de.bixilon.minosoft.util.account -import de.bixilon.kutil.cast.CastUtil.nullCast -import de.bixilon.kutil.cast.CastUtil.unsafeCast import de.bixilon.kutil.uuid.UUIDUtil.trim -import de.bixilon.minosoft.data.accounts.MojangAccountInfo +import de.bixilon.minosoft.data.accounts.types.microsoft.MinecraftTokens import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition +import de.bixilon.minosoft.util.account.microsoft.minecraft.MinecraftAPIException +import de.bixilon.minosoft.util.account.microsoft.minecraft.MinecraftProfile import de.bixilon.minosoft.util.http.HTTP2.getJson import de.bixilon.minosoft.util.http.HTTP2.postJson -import de.bixilon.minosoft.util.http.exceptions.AuthenticationException +import de.bixilon.minosoft.util.json.Jackson import de.bixilon.minosoft.util.logging.Log import de.bixilon.minosoft.util.logging.LogLevels import de.bixilon.minosoft.util.logging.LogMessageType @@ -29,24 +29,16 @@ import java.util.* object AccountUtil { private const val MOJANG_URL_JOIN = "https://sessionserver.mojang.com/session/minecraft/join" - fun getMojangAccountInfo(bearerToken: String): MojangAccountInfo { + fun fetchMinecraftProfile(token: MinecraftTokens): MinecraftProfile { val response = ProtocolDefinition.MICROSOFT_ACCOUNT_GET_MOJANG_PROFILE_URL.getJson(mapOf( - "Authorization" to "Bearer $bearerToken" + "Authorization" to "Bearer ${token.accessToken}", )) - response.body!! if (response.statusCode != 200) { - val errorMessage = when (response.statusCode) { - 404 -> "You don't have a copy of minecraft!" - else -> response.body["errorMessage"].unsafeCast() - } - throw LoginException(response.statusCode, "Could not get minecraft profile", errorMessage) + throw MinecraftAPIException(response) // 404 means that the account has not purchased minecraft } - return MojangAccountInfo( - id = response.body["id"].unsafeCast(), - name = response.body["name"].unsafeCast(), - ) + return Jackson.MAPPER.convertValue(response.body, MinecraftProfile::class.java) } fun joinMojangServer(username: String, accessToken: String, selectedProfile: UUID, serverId: String) { @@ -57,9 +49,8 @@ object AccountUtil { ).postJson(MOJANG_URL_JOIN) - if (response.statusCode != 204) { - response.body!! - throw AuthenticationException(response.statusCode, response.body["errorMessage"]?.nullCast()) + if (response.statusCode != 204 && response.statusCode != 200) { + throw MinecraftAPIException(response) } Log.log(LogMessageType.AUTHENTICATION, LogLevels.VERBOSE) { "Mojang server join successful (username=$username, serverId=$serverId)" } diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/AuthenticationResponse.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/AuthenticationResponse.kt new file mode 100644 index 000000000..ac9c7c232 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/AuthenticationResponse.kt @@ -0,0 +1,38 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft + +import de.bixilon.kutil.time.TimeUtil +import de.bixilon.minosoft.data.accounts.types.microsoft.MicrosoftTokens + +class AuthenticationResponse( + val tokenType: TokenTypes, + scope: String, + expiresIn: Int, + val accessToken: String, + val idToken: String?, + val refreshToken: String, +) { + val expires: Long = (TimeUtil.time / 1000L) + expiresIn + val scope = scope.split(' ') + + fun saveTokens(): MicrosoftTokens { + return MicrosoftTokens(accessToken = accessToken, refreshToken = refreshToken, expires = expires) + } + + enum class TokenTypes { + BEARER, + ; + } +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftAPIError.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftAPIError.kt new file mode 100644 index 000000000..529f066a0 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftAPIError.kt @@ -0,0 +1,25 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft + +import java.util.* + +class MicrosoftAPIError( + val traceId: UUID, + val errorDescription: String, + val correlationId: UUID, + val errorCodes: List, + val error: String, + val timestamp: String, +) diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftAPIException.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftAPIException.kt new file mode 100644 index 000000000..aa02f2e94 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftAPIException.kt @@ -0,0 +1,26 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft + +import de.bixilon.kutil.exception.ExceptionUtil +import de.bixilon.minosoft.util.http.HTTPResponse +import de.bixilon.minosoft.util.json.Jackson + +class MicrosoftAPIException( + val errorCode: Int, + val error: MicrosoftAPIError?, +) : Exception(error?.errorDescription) { + + constructor(response: HTTPResponse?>) : this(response.statusCode, response.body?.let { ExceptionUtil.tryCatch(Throwable::class.java) { Jackson.MAPPER.convertValue(it, MicrosoftAPIError::class.java) } }) +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftOAuthUtils.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftOAuthUtils.kt index 639a089cc..da42429b5 100644 --- a/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftOAuthUtils.kt +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftOAuthUtils.kt @@ -13,78 +13,148 @@ package de.bixilon.minosoft.util.account.microsoft -import de.bixilon.kutil.cast.CastUtil.unsafeCast -import de.bixilon.kutil.json.JsonUtil.asJsonList -import de.bixilon.kutil.json.JsonUtil.asJsonObject -import de.bixilon.kutil.primitive.LongUtil.toLong -import de.bixilon.kutil.uuid.UUIDUtil.toUUID +import de.bixilon.kutil.concurrent.pool.DefaultThreadPool +import de.bixilon.kutil.concurrent.time.TimeWorker +import de.bixilon.kutil.concurrent.time.TimeWorkerTask +import de.bixilon.kutil.time.TimeUtil import de.bixilon.minosoft.data.accounts.AccountStates import de.bixilon.minosoft.data.accounts.types.microsoft.MicrosoftAccount +import de.bixilon.minosoft.data.accounts.types.microsoft.MicrosoftTokens import de.bixilon.minosoft.data.player.properties.PlayerProperties import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.util.account.AccountUtil -import de.bixilon.minosoft.util.account.LoginException +import de.bixilon.minosoft.util.account.microsoft.code.MicrosoftDeviceCode +import de.bixilon.minosoft.util.account.microsoft.minecraft.MinecraftAPIException +import de.bixilon.minosoft.util.account.microsoft.minecraft.MinecraftBearerResponse +import de.bixilon.minosoft.util.account.microsoft.xbox.XSTSToken +import de.bixilon.minosoft.util.account.microsoft.xbox.XboxAPIError +import de.bixilon.minosoft.util.account.microsoft.xbox.XboxAPIException +import de.bixilon.minosoft.util.account.microsoft.xbox.XboxLiveToken import de.bixilon.minosoft.util.http.HTTP2.postData import de.bixilon.minosoft.util.http.HTTP2.postJson +import de.bixilon.minosoft.util.json.Jackson import de.bixilon.minosoft.util.logging.Log import de.bixilon.minosoft.util.logging.LogLevels import de.bixilon.minosoft.util.logging.LogMessageType -import de.bixilon.minosoft.util.url.URLProtocolStreamHandlers -import java.net.URL -import java.net.URLConnection -import java.net.URLStreamHandler +import java.util.concurrent.TimeoutException object MicrosoftOAuthUtils { + const val TENANT = "consumers" + const val DEVICE_CODE_URL = "https://login.microsoftonline.com/$TENANT/oauth2/v2.0/devicecode" + const val TOKEN_CHECK_URL = "https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token" + const val MAX_CHECK_TIME = 900 - fun loginToMicrosoftAccount(authorizationCode: String): MicrosoftAccount { + fun obtainDeviceCodeAsync( + tokenCallback: (MicrosoftDeviceCode) -> Unit, + errorCallback: (Throwable) -> Unit, + successCallback: (AuthenticationResponse) -> Unit, + ) { + DefaultThreadPool += { + val deviceCode = obtainDeviceCode() + Log.log(LogMessageType.AUTHENTICATION, LogLevels.INFO) { "Obtained device code: ${deviceCode.userCode}" } + tokenCallback(deviceCode) + val start = TimeUtil.time / 1000 + + fun checkToken() { + try { + val response = checkDeviceCode(deviceCode) + val time = TimeUtil.time / 1000 + if (time > start + MAX_CHECK_TIME || time > deviceCode.expires) { + throw TimeoutException("Could not obtain access for device code: ${deviceCode.userCode}") + } + if (response == null) { + // no response yet + TimeWorker += TimeWorkerTask(deviceCode.interval * 1000, true) { checkToken() } + return + } + Log.log(LogMessageType.AUTHENTICATION, LogLevels.INFO) { "Code (${deviceCode.userCode}) is valid, logging in..." } + successCallback(response) + } catch (exception: Throwable) { + exception.printStackTrace() + errorCallback(exception) + } + } + checkToken() + } + } + + fun obtainDeviceCode(): MicrosoftDeviceCode { + val response = mapOf( + "client_id" to ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID, + "scope" to "XboxLive.signin offline_access", + ).postData(DEVICE_CODE_URL) + + if (response.statusCode != 200) { + throw MicrosoftAPIException(response) + } + + return Jackson.MAPPER.convertValue(response.body, MicrosoftDeviceCode::class.java) + } + + fun checkDeviceCode(deviceCode: MicrosoftDeviceCode): AuthenticationResponse? { + val response = mapOf( + "grant_type" to "urn:ietf:params:oauth:grant-type:device_code", + "client_id" to ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID, + "device_code" to deviceCode.deviceCode, + ).postData(TOKEN_CHECK_URL) + + if (response.statusCode != 200) { + val error = MicrosoftAPIException(response) + if (error.error?.error == "authorization_pending") { + return null + } + throw error + } + + return Jackson.MAPPER.convertValue(response.body, AuthenticationResponse::class.java) + } + + fun refreshToken(token: MicrosoftTokens): AuthenticationResponse { + val response = mapOf( + "client_id" to ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID, + "grant_type" to "refresh_token", + "scope" to "XboxLive.signin offline_access", + "refresh_token" to token.refreshToken, + ).postData(TOKEN_CHECK_URL) + + if (response.statusCode != 200) { + throw MicrosoftAPIException(response) + } + + return Jackson.MAPPER.convertValue(response.body, AuthenticationResponse::class.java) + } + + fun loginToMicrosoftAccount(response: AuthenticationResponse): MicrosoftAccount { Log.log(LogMessageType.AUTHENTICATION, LogLevels.INFO) { "Logging into microsoft account..." } - val authorizationToken = getAuthorizationToken(authorizationCode) - val (xboxLiveToken, userHash) = getXboxLiveToken(authorizationToken) + val msaTokens = response.saveTokens() + val xboxLiveToken = getXboxLiveToken(msaTokens) val xstsToken = getXSTSToken(xboxLiveToken) - val accessToken = getMinecraftBearerAccessToken(userHash, xstsToken) - val accountInfo = AccountUtil.getMojangAccountInfo(accessToken) + val minecraftToken = getMinecraftBearerAccessToken(xboxLiveToken, xstsToken).saveTokens() + val profile = AccountUtil.fetchMinecraftProfile(minecraftToken) + + val playerProperties = PlayerProperties.fetch(profile.uuid) - val uuid = accountInfo.id.toUUID() val account = MicrosoftAccount( - uuid = uuid, - username = accountInfo.name, - authorizationToken = authorizationToken, - properties = PlayerProperties.fetch(uuid), + uuid = profile.uuid, + username = profile.name, + msa = msaTokens, + minecraft = minecraftToken, + properties = playerProperties, ) account.state = AccountStates.WORKING - account.accessToken = accessToken - account.check(null, "") // client token does not exist for microsoft accounts - - Log.log(LogMessageType.AUTHENTICATION, LogLevels.INFO) { "Microsoft account login successful (uuid=${account.uuid})" } + Log.log(LogMessageType.AUTHENTICATION, LogLevels.INFO) { "Microsoft account login successful (username=${account.username}, uuid=${account.uuid})" } return account } - fun getAuthorizationToken(authorizationCode: String): String { - val response = 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", - ).postData(ProtocolDefinition.MICROSOFT_ACCOUNT_AUTH_TOKEN_URL) - if (response.statusCode != 200) { - throw LoginException(response.statusCode, "Could not get authorization token", response.body.toString()) - } - response.body!! - return response.body["access_token"].unsafeCast() - } - - /** - * returns A: XBL Token; B: UHS Token - */ - fun getXboxLiveToken(authorizationToken: String): Pair { + fun getXboxLiveToken(msaTokens: MicrosoftTokens): XboxLiveToken { val response = mapOf( "Properties" to mapOf( "AuthMethod" to "RPS", "SiteName" to "user.auth.xboxlive.com", - "RpsTicket" to authorizationToken + "RpsTicket" to "d=${msaTokens.accessToken}", ), "RelyingParty" to "http://auth.xboxlive.com", "TokenType" to "JWT", @@ -92,56 +162,46 @@ object MicrosoftOAuthUtils { if (response.statusCode != 200 || response.body == null) { - throw LoginException(response.statusCode, "Could not authenticate with xbox live token", response.body.toString()) + throw XboxAPIException(response) } - return Pair(response.body["Token"].unsafeCast(), response.body["DisplayClaims"].asJsonObject()["xui"].asJsonList()[0].asJsonObject()["uhs"].unsafeCast()) + return Jackson.MAPPER.convertValue(response.body, XboxLiveToken::class.java) } - fun getXSTSToken(xBoxLiveToken: String): String { + fun getXSTSToken(xBoxLiveToken: XboxLiveToken): XSTSToken { val response = mapOf( "Properties" to mapOf( "SandboxId" to "RETAIL", - "UserTokens" to listOf(xBoxLiveToken) + "UserTokens" to listOf(xBoxLiveToken.token) ), "RelyingParty" to "rp://api.minecraftservices.com/", "TokenType" to "JWT", ).postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_XSTS_URL) - response.body!! if (response.statusCode != 200) { - val errorMessage = when (response.body["XErr"].toLong()) { + val error = Jackson.MAPPER.convertValue(response.body, XboxAPIError::class.java) + val errorMessage = when (error.error) { 2148916233 -> "You don't have an XBox account!" + 2148916235 -> "Xbox Live is banned in your country!" + 2148916236, 2148916237 -> "Your account needs adult verification (South Korea)" 2148916238 -> "This account is a child account!" - else -> response.body["Message"].unsafeCast() + else -> error.message ?: "Unknown" } - throw LoginException(response.statusCode, "Could not get xsts token", errorMessage) + throw XboxAPIException(response.statusCode, error, errorMessage) } - return response.body["Token"].unsafeCast() + + return Jackson.MAPPER.convertValue(response.body, XSTSToken::class.java) } - fun getMinecraftBearerAccessToken(userHash: String, xstsToken: String): String { + fun getMinecraftBearerAccessToken(xBoxLiveToken: XboxLiveToken, xstsToken: XSTSToken): MinecraftBearerResponse { val response = mapOf( - "identityToken" to "XBL3.0 x=${userHash};${xstsToken}", + "identityToken" to "XBL3.0 x=${xBoxLiveToken.userHash};${xstsToken.token}", "ensureLegacyEnabled" to true, ).postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_MINECRAFT_LOGIN_WITH_XBOX_URL) - response.body!! if (response.statusCode != 200) { - throw LoginException(response.statusCode, "Could not get minecraft access token ", (response.body["errorMessage"] ?: response.body["error"] ?: "unknown").unsafeCast()) - } - return response.body["access_token"].unsafeCast() - } - - - init { - URLProtocolStreamHandlers.PROTOCOLS["ms-xal-" + ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID] = LoginURLHandler - } - - private object LoginURLHandler : URLStreamHandler() { - - override fun openConnection(url: URL): URLConnection { - return URLProtocolStreamHandlers.NULL_URL_CONNECTION + throw MinecraftAPIException(response) } + return Jackson.MAPPER.convertValue(response.body, MinecraftBearerResponse::class.java) } } diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/code/MicrosoftDeviceCode.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/code/MicrosoftDeviceCode.kt new file mode 100644 index 000000000..d682341ff --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/code/MicrosoftDeviceCode.kt @@ -0,0 +1,34 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.code + +import de.bixilon.kutil.time.TimeUtil +import de.bixilon.minosoft.data.text.URLProtocols +import java.net.URL + +data class MicrosoftDeviceCode( + val deviceCode: String, + val userCode: String, + val verificationURI: URL, + val expiresIn: Int, + val interval: Int, + val message: String, +) { + val expires = (TimeUtil.time / 1000) + expiresIn + + init { + check(verificationURI.protocol == URLProtocols.HTTPS.protocol) { "Insecure url: $verificationURI" } + check(verificationURI.host == "login.microsoftonline.com" || verificationURI.host == "www.microsoft.com" || verificationURI.host == "microsoft.com") { "Invalid verification host: $verificationURI" } + } +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftAPIError.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftAPIError.kt new file mode 100644 index 000000000..b562148fc --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftAPIError.kt @@ -0,0 +1,22 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.minecraft + +class MinecraftAPIError( + val path: String, + val errorType: String, + val error: String, + val errorMessage: String, + val developerMessage: String, +) diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftAPIException.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftAPIException.kt new file mode 100644 index 000000000..a557bffc7 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftAPIException.kt @@ -0,0 +1,26 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.minecraft + +import de.bixilon.kutil.exception.ExceptionUtil +import de.bixilon.minosoft.util.http.HTTPResponse +import de.bixilon.minosoft.util.json.Jackson + +class MinecraftAPIException( + val errorCode: Int, + val error: MinecraftAPIError?, +) : Exception(error?.developerMessage) { + + constructor(response: HTTPResponse?>) : this(response.statusCode, response.body?.let { ExceptionUtil.tryCatch(Throwable::class.java) { Jackson.MAPPER.convertValue(it, MinecraftAPIError::class.java) } }) +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftBearerResponse.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftBearerResponse.kt new file mode 100644 index 000000000..0dfbfc30c --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftBearerResponse.kt @@ -0,0 +1,34 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.minecraft + +import de.bixilon.kutil.time.TimeUtil +import de.bixilon.minosoft.data.accounts.types.microsoft.MinecraftTokens +import de.bixilon.minosoft.util.account.microsoft.AuthenticationResponse +import java.util.* + +data class MinecraftBearerResponse( + val username: UUID, + val roles: List, + val accessToken: String, + val tokenType: AuthenticationResponse.TokenTypes, + val expiresIn: Int, +) { + val expires = (TimeUtil.time / 1000) + expiresIn + + + fun saveTokens(): MinecraftTokens { + return MinecraftTokens(accessToken = accessToken, expires = expires) + } +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftProfile.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftProfile.kt new file mode 100644 index 000000000..d48590ffe --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/minecraft/MinecraftProfile.kt @@ -0,0 +1,25 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.minecraft + +import de.bixilon.kutil.uuid.UUIDUtil.toUUID + +data class MinecraftProfile( + val id: String, + val name: String, + val skins: Any?, + val capes: Any?, +) { + val uuid = id.toUUID() +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XSTSToken.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XSTSToken.kt new file mode 100644 index 000000000..d82cf15b0 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XSTSToken.kt @@ -0,0 +1,28 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.xbox + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import de.bixilon.kutil.json.JsonUtil.asJsonList +import de.bixilon.kutil.json.JsonUtil.asJsonObject + +data class XSTSToken( + @JsonProperty("IssueInstant") val issueInstant: String, + @JsonProperty("NotAfter") val notAfter: String, + @JsonProperty("Token") val token: String, + @JsonProperty("DisplayClaims") val displayClaims: Map, +) { + @JsonIgnore val userHash = displayClaims["xui"].asJsonList()[0].asJsonObject()["uhs"] +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxAPIError.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxAPIError.kt new file mode 100644 index 000000000..3e8524bd6 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxAPIError.kt @@ -0,0 +1,23 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.xbox + +import com.fasterxml.jackson.annotation.JsonProperty + +data class XboxAPIError( + @JsonProperty("Identity") val identity: Int, + @JsonProperty("XErr") val error: Long, + @JsonProperty("Message") val message: String?, + @JsonProperty("Redirect") val redirect: String?, +) diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxAPIException.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxAPIException.kt new file mode 100644 index 000000000..ef94eb708 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxAPIException.kt @@ -0,0 +1,27 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.xbox + +import de.bixilon.kutil.exception.ExceptionUtil +import de.bixilon.minosoft.util.http.HTTPResponse +import de.bixilon.minosoft.util.json.Jackson + +class XboxAPIException( + val errorCode: Int, + val error: XboxAPIError?, + message: String? = error?.error?.toString(), +) : Exception(message) { + + constructor(response: HTTPResponse?>) : this(response.statusCode, response.body?.let { ExceptionUtil.tryCatch(Throwable::class.java) { Jackson.MAPPER.convertValue(it, XboxAPIError::class.java) } }) +} diff --git a/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxLiveToken.kt b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxLiveToken.kt new file mode 100644 index 000000000..7477a4d0a --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/account/microsoft/xbox/XboxLiveToken.kt @@ -0,0 +1,28 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util.account.microsoft.xbox + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import de.bixilon.kutil.json.JsonUtil.asJsonList +import de.bixilon.kutil.json.JsonUtil.asJsonObject + +data class XboxLiveToken( + @JsonProperty("IssueInstant") val issueInstant: String, + @JsonProperty("NotAfter") val notAfter: String, + @JsonProperty("Token") val token: String, + @JsonProperty("DisplayClaims") val displayClaims: Map, +) { + @JsonIgnore val userHash = displayClaims["xui"].asJsonList()[0].asJsonObject()["uhs"] +} diff --git a/src/main/resources/assets/minosoft/eros/main/account/add/microsoft.fxml b/src/main/resources/assets/minosoft/eros/main/account/add/microsoft.fxml index f36874179..3b578e508 100644 --- a/src/main/resources/assets/minosoft/eros/main/account/add/microsoft.fxml +++ b/src/main/resources/assets/minosoft/eros/main/account/add/microsoft.fxml @@ -14,29 +14,46 @@ --> + - + + + - + - + + + + + + + + + + + diff --git a/src/main/resources/assets/minosoft/language/en_us.lang b/src/main/resources/assets/minosoft/language/en_us.lang index 9ad67c9f2..788a16910 100644 --- a/src/main/resources/assets/minosoft/language/en_us.lang +++ b/src/main/resources/assets/minosoft/language/en_us.lang @@ -128,8 +128,9 @@ minosoft:main.account.add.mojang.password.placeholder=******** minosoft:main.account.add.mojang.add_button=Add minosoft:main.account.add.mojang.cancel_button=Cancel -minosoft:main.account.add.microsoft.title=Add microsoft account - +minosoft:main.account.add.microsoft.title=Add microsoft account - Minosoft +minosoft:main.account.add.microsoft.header=Please use a web browser to open the page %1$s and enter the following code in order to proceed with the login +minosoft:main.account.add.microsoft.cancel=Cancel minosoft:connection.kick.title=Kicked from server minosoft:connection.kick.header=You got kicked