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