mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-18 20:05:02 -04:00
improve microsoft accounts
This commit is contained in:
parent
feaebc9e16
commit
dba5c12b76
@ -63,4 +63,8 @@ abstract class Account(
|
||||
}
|
||||
check(latch, clientToken)
|
||||
}
|
||||
|
||||
fun save() {
|
||||
println("ToDo") // ToDo
|
||||
}
|
||||
}
|
||||
|
@ -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}"
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -97,6 +97,7 @@ class MojangAccount(
|
||||
|
||||
refreshed = true
|
||||
state = AccountStates.WORKING
|
||||
save()
|
||||
Log.log(LogMessageType.AUTHENTICATION, LogLevels.VERBOSE) { "Mojang account refresh successful (username=$username)" }
|
||||
}
|
||||
|
||||
|
@ -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<URLProtocols> {
|
||||
|
@ -253,8 +253,8 @@ class AccountController : EmbeddedJavaFXController<Pane>() {
|
||||
"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<OfflineAccount>(
|
||||
resourceLocation = OfflineAccount.RESOURCE_LOCATION,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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)" }
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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,
|
||||
;
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Int>,
|
||||
val error: String,
|
||||
val timestamp: String,
|
||||
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Map<String, Any>?>) : this(response.statusCode, response.body?.let { ExceptionUtil.tryCatch(Throwable::class.java) { Jackson.MAPPER.convertValue(it, MicrosoftAPIError::class.java) } })
|
||||
}
|
@ -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<String, String> {
|
||||
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)
|
||||
}
|
||||
return response.body["Token"].unsafeCast()
|
||||
throw XboxAPIException(response.statusCode, error, errorMessage)
|
||||
}
|
||||
|
||||
fun getMinecraftBearerAccessToken(userHash: String, xstsToken: String): String {
|
||||
return Jackson.MAPPER.convertValue(response.body, XSTSToken::class.java)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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" }
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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,
|
||||
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Map<String, Any>?>) : this(response.statusCode, response.body?.let { ExceptionUtil.tryCatch(Throwable::class.java) { Jackson.MAPPER.convertValue(it, MinecraftAPIError::class.java) } })
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Any>,
|
||||
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)
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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()
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<String, Any>,
|
||||
) {
|
||||
@JsonIgnore val userHash = displayClaims["xui"].asJsonList()[0].asJsonObject()["uhs"]
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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?,
|
||||
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Map<String, Any>?>) : this(response.statusCode, response.body?.let { ExceptionUtil.tryCatch(Throwable::class.java) { Jackson.MAPPER.convertValue(it, XboxAPIError::class.java) } })
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<String, Any>,
|
||||
) {
|
||||
@JsonIgnore val userHash = displayClaims["xui"].asJsonList()[0].asJsonObject()["uhs"]
|
||||
}
|
@ -14,29 +14,46 @@
|
||||
-->
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="root" prefHeight="90.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/17"> <!-- fx:controller="de.bixilon.minosoft.gui.eros.main.account.add.MicrosoftAddController"-->
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="root" prefHeight="120.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/18"> <!-- fx:controller="de.bixilon.minosoft.gui.eros.main.account.add.MicrosoftAddController"-->
|
||||
<GridPane HBox.hgrow="ALWAYS">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="ALWAYS"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="NEVER"/>
|
||||
<RowConstraints vgrow="NEVER"/>
|
||||
<RowConstraints vgrow="ALWAYS"/>
|
||||
<RowConstraints vgrow="NEVER"/>
|
||||
</rowConstraints>
|
||||
<TextFlow fx:id="textFX" prefHeight="200.0" prefWidth="200.0">
|
||||
<TextFlow fx:id="headerFX" prefHeight="200.0" prefWidth="200.0">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
|
||||
</GridPane.margin>
|
||||
<Text text="Please open https://microsoft.com/link and enter the following code to proceed with the login:"/>
|
||||
</TextFlow>
|
||||
<TextField fx:id="codeFX" editable="false" promptText="Loading...." GridPane.rowIndex="1">
|
||||
<TextField fx:id="codeFX" alignment="CENTER" editable="false" promptText="DUMMY-CODE" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</GridPane.margin>
|
||||
</TextField>
|
||||
<GridPane GridPane.rowIndex="3">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="ALWAYS"/>
|
||||
<ColumnConstraints hgrow="NEVER"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="NEVER"/>
|
||||
</rowConstraints>
|
||||
<Button fx:id="cancelFX" onAction="#cancel" disable="true" text="Cancel" GridPane.columnIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
</GridPane>
|
||||
</GridPane>
|
||||
</HBox>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user