summaryrefslogtreecommitdiff
path: root/meshmc/launcher/minecraft/auth/steps
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/minecraft/auth/steps
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/minecraft/auth/steps')
-rw-r--r--meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp80
-rw-r--r--meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h47
-rw-r--r--meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp64
-rw-r--r--meshmc/launcher/minecraft/auth/steps/GetSkinStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MSAStep.cpp161
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MSAStep.h55
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp98
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp101
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp191
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h55
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp96
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp93
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxUserStep.h44
16 files changed, 1261 insertions, 0 deletions
diff --git a/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp
new file mode 100644
index 0000000000..8d0418042a
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp
@@ -0,0 +1,80 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "EntitlementsStep.h"
+
+#include <QNetworkRequest>
+#include <QUuid>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {}
+
+EntitlementsStep::~EntitlementsStep() noexcept = default;
+
+QString EntitlementsStep::describe()
+{
+ return tr("Determining game ownership.");
+}
+
+void EntitlementsStep::perform()
+{
+ auto uuid = QUuid::createUuid();
+ m_entitlementsRequestId = uuid.toString().remove('{').remove('}');
+ auto url =
+ "https://api.minecraftservices.com/entitlements/license?requestId=" +
+ m_entitlementsRequestId;
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader(
+ "Authorization",
+ QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &EntitlementsStep::onRequestDone);
+ requestor->get(request);
+ qDebug() << "Getting entitlements...";
+}
+
+void EntitlementsStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void EntitlementsStep::onRequestDone(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ // TODO: check presence of same entitlementsRequestId?
+ // TODO: validate JWTs?
+ Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
+
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h
new file mode 100644
index 0000000000..bd97a1c59e
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h
@@ -0,0 +1,47 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class EntitlementsStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit EntitlementsStep(AccountData* data);
+ virtual ~EntitlementsStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+
+ private:
+ QString m_entitlementsRequestId;
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp
new file mode 100644
index 0000000000..abf5db950f
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp
@@ -0,0 +1,64 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "GetSkinStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {}
+
+GetSkinStep::~GetSkinStep() noexcept = default;
+
+QString GetSkinStep::describe()
+{
+ return tr("Getting skin.");
+}
+
+void GetSkinStep::perform()
+{
+ auto url = QUrl(m_data->minecraftProfile.skin.url);
+ QNetworkRequest request = QNetworkRequest(url);
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &GetSkinStep::onRequestDone);
+ requestor->get(request);
+}
+
+void GetSkinStep::rehydrate()
+{
+ // NOOP, for now.
+}
+
+void GetSkinStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error == QNetworkReply::NoError) {
+ m_data->minecraftProfile.skin.data = data;
+ }
+ emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/GetSkinStep.h b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.h
new file mode 100644
index 0000000000..ed6a288cdb
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class GetSkinStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit GetSkinStep(AccountData* data);
+ virtual ~GetSkinStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/MSAStep.cpp b/meshmc/launcher/minecraft/auth/steps/MSAStep.cpp
new file mode 100644
index 0000000000..9be4761549
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MSAStep.cpp
@@ -0,0 +1,161 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "MSAStep.h"
+
+#include <QNetworkRequest>
+#include <QDesktopServices>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+#include "Application.h"
+
+MSAStep::MSAStep(AccountData* data, Action action)
+ : AuthStep(data), m_action(action)
+{
+ m_replyHandler = new QOAuthHttpServerReplyHandler(this);
+ m_replyHandler->setCallbackText(
+ tr("Login successful! You can close this page and return to MeshMC."));
+
+ m_oauth2 = new QOAuth2AuthorizationCodeFlow(this);
+ m_oauth2->setClientIdentifier(APPLICATION->msaClientId());
+ m_oauth2->setAuthorizationUrl(QUrl(
+ "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
+ m_oauth2->setTokenUrl(
+ QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
+ m_oauth2->setScope("XboxLive.signin offline_access");
+ m_oauth2->setReplyHandler(m_replyHandler);
+ m_oauth2->setNetworkAccessManager(APPLICATION->network().get());
+
+ connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this,
+ &MSAStep::onGranted);
+ connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this,
+ &MSAStep::onRequestFailed);
+ connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this,
+ &MSAStep::onOpenBrowser);
+}
+
+MSAStep::~MSAStep() noexcept = default;
+
+QString MSAStep::describe()
+{
+ return tr("Logging in with Microsoft account.");
+}
+
+void MSAStep::rehydrate()
+{
+ switch (m_action) {
+ case Refresh: {
+ // TODO: check the tokens and see if they are old (older than a day)
+ return;
+ }
+ case Login: {
+ // NOOP
+ return;
+ }
+ }
+}
+
+void MSAStep::perform()
+{
+ switch (m_action) {
+ case Refresh: {
+ // Load the refresh token from stored account data
+ m_oauth2->setRefreshToken(m_data->msaToken.refresh_token);
+ m_oauth2->refreshTokens();
+ return;
+ }
+ case Login: {
+ *m_data = AccountData();
+ if (!m_replyHandler->isListening()) {
+ if (!m_replyHandler->listen(QHostAddress::LocalHost)) {
+ emit finished(AccountTaskState::STATE_FAILED_HARD,
+ tr("Failed to start local HTTP server for "
+ "OAuth2 callback."));
+ return;
+ }
+ }
+ m_oauth2->setModifyParametersFunction(
+ [](QAbstractOAuth::Stage stage,
+ QMultiMap<QString, QVariant>* parameters) {
+ if (stage ==
+ QAbstractOAuth::Stage::RequestingAuthorization) {
+ parameters->insert("prompt", "select_account");
+ }
+ });
+ m_oauth2->grant();
+ return;
+ }
+ }
+}
+
+void MSAStep::onOpenBrowser(const QUrl& url)
+{
+ emit authorizeWithBrowser(url);
+ QDesktopServices::openUrl(url);
+}
+
+void MSAStep::onGranted()
+{
+ m_replyHandler->close();
+
+ // Store the tokens in account data
+ m_data->msaToken.token = m_oauth2->token();
+ m_data->msaToken.refresh_token = m_oauth2->refreshToken();
+ m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
+ m_data->msaToken.notAfter = m_oauth2->expirationAt();
+ if (!m_data->msaToken.notAfter.isValid()) {
+ m_data->msaToken.notAfter = m_data->msaToken.issueInstant.addSecs(3600);
+ }
+ m_data->msaToken.validity = Katabasis::Validity::Certain;
+
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token."));
+}
+
+void MSAStep::onRequestFailed(QAbstractOAuth::Error error)
+{
+ m_replyHandler->close();
+
+ switch (error) {
+ case QAbstractOAuth::Error::NetworkError:
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Microsoft authentication failed due to a network error."));
+ return;
+ case QAbstractOAuth::Error::ServerError:
+ case QAbstractOAuth::Error::OAuthTokenNotFoundError:
+ case QAbstractOAuth::Error::OAuthTokenSecretNotFoundError:
+ case QAbstractOAuth::Error::OAuthCallbackNotVerified:
+ emit finished(AccountTaskState::STATE_FAILED_HARD,
+ tr("Microsoft authentication failed."));
+ return;
+ case QAbstractOAuth::Error::ExpiredError:
+ emit finished(AccountTaskState::STATE_FAILED_GONE,
+ tr("Microsoft authentication token expired."));
+ return;
+ default:
+ emit finished(AccountTaskState::STATE_FAILED_HARD,
+ tr("Microsoft authentication failed with an "
+ "unrecognized error."));
+ return;
+ }
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/MSAStep.h b/meshmc/launcher/minecraft/auth/steps/MSAStep.h
new file mode 100644
index 0000000000..2e223024e3
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MSAStep.h
@@ -0,0 +1,55 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+#include <QOAuth2AuthorizationCodeFlow>
+#include <QOAuthHttpServerReplyHandler>
+
+class MSAStep : public AuthStep
+{
+ Q_OBJECT
+ public:
+ enum Action { Refresh, Login };
+
+ public:
+ explicit MSAStep(AccountData* data, Action action);
+ virtual ~MSAStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onGranted();
+ void onRequestFailed(QAbstractOAuth::Error error);
+ void onOpenBrowser(const QUrl& url);
+
+ private:
+ QOAuth2AuthorizationCodeFlow* m_oauth2 = nullptr;
+ QOAuthHttpServerReplyHandler* m_replyHandler = nullptr;
+ Action m_action;
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp
new file mode 100644
index 0000000000..19afcda3fc
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp
@@ -0,0 +1,98 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "MeshMCLoginStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+#include "minecraft/auth/AccountTask.h"
+
+MeshMCLoginStep::MeshMCLoginStep(AccountData* data) : AuthStep(data) {}
+
+MeshMCLoginStep::~MeshMCLoginStep() noexcept = default;
+
+QString MeshMCLoginStep::describe()
+{
+ return tr("Accessing Mojang services.");
+}
+
+void MeshMCLoginStep::perform()
+{
+ auto requestURL = "https://api.minecraftservices.com/launcher/login";
+ auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
+ auto xToken = m_data->mojangservicesToken.token;
+
+ QString mc_auth_template = R"XXX(
+{
+ "xtoken": "XBL3.0 x=%1;%2",
+ "platform": "PC_LAUNCHER"
+}
+)XXX";
+ auto requestBody = mc_auth_template.arg(uhs, xToken);
+
+ QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &MeshMCLoginStep::onRequestDone);
+ requestor->post(request, requestBody.toUtf8());
+ qDebug() << "Getting Minecraft access token...";
+}
+
+void MeshMCLoginStep::rehydrate()
+{
+ // TODO: check the token validity
+}
+
+void MeshMCLoginStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ qDebug() << data;
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to get Minecraft access token: %1")
+ .arg(requestor->errorString_));
+ return;
+ }
+
+ if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
+ qWarning() << "Could not parse login_with_xbox response...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to parse the Minecraft access token response."));
+ return;
+ }
+ emit finished(AccountTaskState::STATE_WORKING, tr(""));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h
new file mode 100644
index 0000000000..859ae867f3
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class MeshMCLoginStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit MeshMCLoginStep(AccountData* data);
+ virtual ~MeshMCLoginStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
new file mode 100644
index 0000000000..9955ff9738
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -0,0 +1,101 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "MinecraftProfileStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data)
+{
+}
+
+MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
+
+QString MinecraftProfileStep::describe()
+{
+ return tr("Fetching the Minecraft profile.");
+}
+
+void MinecraftProfileStep::perform()
+{
+ auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader(
+ "Authorization",
+ QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
+
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &MinecraftProfileStep::onRequestDone);
+ requestor->get(request);
+}
+
+void MinecraftProfileStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void MinecraftProfileStep::onRequestDone(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ if (error == QNetworkReply::ContentNotFoundError) {
+ // NOTE: Succeed even if we do not have a profile. This is a valid
+ // account state.
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(AccountTaskState::STATE_SUCCEEDED,
+ tr("Account has no Minecraft profile."));
+ return;
+ }
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Error getting profile:";
+ qWarning() << " HTTP Status: " << requestor->httpStatus_;
+ qWarning() << " Internal error no.: " << error;
+ qWarning() << " Error string: " << requestor->errorString_;
+
+ qWarning() << " Response:";
+ qWarning() << QString::fromUtf8(data);
+
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile acquisition failed."));
+ return;
+ }
+ if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile response could not be parsed"));
+ return;
+ }
+
+ emit finished(AccountTaskState::STATE_WORKING,
+ tr("Minecraft Java profile acquisition succeeded."));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h
new file mode 100644
index 0000000000..eb0594bdf8
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class MinecraftProfileStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit MinecraftProfileStep(AccountData* data);
+ virtual ~MinecraftProfileStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
new file mode 100644
index 0000000000..b54ad2a32b
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
@@ -0,0 +1,191 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "XboxAuthorizationStep.h"
+
+#include <QNetworkRequest>
+#include <QJsonParseError>
+#include <QJsonDocument>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data,
+ Katabasis::Token* token,
+ QString relyingParty,
+ QString authorizationKind)
+ : AuthStep(data), m_token(token), m_relyingParty(relyingParty),
+ m_authorizationKind(authorizationKind)
+{
+}
+
+XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default;
+
+QString XboxAuthorizationStep::describe()
+{
+ return tr("Getting authorization to access %1 services.")
+ .arg(m_authorizationKind);
+}
+
+void XboxAuthorizationStep::rehydrate()
+{
+ // FIXME: check if the tokens are good?
+}
+
+void XboxAuthorizationStep::perform()
+{
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "SandboxId": "RETAIL",
+ "UserTokens": [
+ "%1"
+ ]
+ },
+ "RelyingParty": "%2",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data =
+ xbox_auth_template.arg(m_data->userToken.token, m_relyingParty);
+ // http://xboxlive.com
+ QNetworkRequest request =
+ QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &XboxAuthorizationStep::onRequestDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "Getting authorization token for " << m_relyingParty;
+}
+
+void XboxAuthorizationStep::onRequestDone(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ if (!processSTSError(error, data, headers)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to get authorization for %1 services. Error %1.")
+ .arg(m_authorizationKind, error));
+ }
+ return;
+ }
+
+ Katabasis::Token temp;
+ if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Could not parse authorization response for access to "
+ "%1 services.")
+ .arg(m_authorizationKind));
+ return;
+ }
+
+ if (temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Server has changed %1 authorization user hash in the "
+ "reply. Something is wrong.")
+ .arg(m_authorizationKind));
+ return;
+ }
+ auto& token = *m_token;
+ token = temp;
+
+ emit finished(AccountTaskState::STATE_WORKING,
+ tr("Got authorization to access %1").arg(m_relyingParty));
+}
+
+bool XboxAuthorizationStep::processSTSError(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ if (error == QNetworkReply::AuthenticationRequiredError) {
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Cannot parse error XSTS response as JSON: "
+ << jsonError.errorString();
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Cannot parse %1 authorization error response as JSON: %2")
+ .arg(m_authorizationKind, jsonError.errorString()));
+ return true;
+ }
+
+ int64_t errorCode = -1;
+ auto obj = doc.object();
+ if (!Parsers::getNumber(obj.value("XErr"), errorCode)) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XErr element is missing from %1 authorization "
+ "error response.")
+ .arg(m_authorizationKind));
+ return true;
+ }
+ switch (errorCode) {
+ case 2148916233: {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("This Microsoft account does not have an XBox Live "
+ "profile. Buy the game on %1 first.")
+ .arg("<a "
+ "href=\"https://www.minecraft.net/en-us/store/"
+ "minecraft-java-edition\">minecraft.net</a>"));
+ return true;
+ }
+ case 2148916235: {
+ // NOTE: this is the Grulovia error
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox Live is not available in your country. "
+ "You've been blocked."));
+ return true;
+ }
+ case 2148916238: {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("This Microsoft account is underaged and is not linked "
+ "to a family.\n\nPlease set up your account according "
+ "to %1.")
+ .arg(
+ "<a "
+ "href=\"https://help.minecraft.net/hc/en-us/"
+ "articles/4403181904525\">help.minecraft.net</a>"));
+ return true;
+ }
+ default: {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XSTS authentication ended with unrecognized "
+ "error(s):\n\n%1")
+ .arg(errorCode));
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
new file mode 100644
index 0000000000..a8413c939f
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
@@ -0,0 +1,55 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class XboxAuthorizationStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit XboxAuthorizationStep(AccountData* data, Katabasis::Token* token,
+ QString relyingParty,
+ QString authorizationKind);
+ virtual ~XboxAuthorizationStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private:
+ bool processSTSError(QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers);
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+
+ private:
+ Katabasis::Token* m_token;
+ QString m_relyingParty;
+ QString m_authorizationKind;
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp
new file mode 100644
index 0000000000..aae94b0403
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp
@@ -0,0 +1,96 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "XboxProfileStep.h"
+
+#include <QNetworkRequest>
+#include <QUrlQuery>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {}
+
+XboxProfileStep::~XboxProfileStep() noexcept = default;
+
+QString XboxProfileStep::describe()
+{
+ return tr("Fetching Xbox profile.");
+}
+
+void XboxProfileStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void XboxProfileStep::perform()
+{
+ auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
+ QUrlQuery q;
+ q.addQueryItem(
+ "settings",
+ "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
+ "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,"
+ "ModernGamertagSuffix,"
+ "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
+ "PreferredColor,Location,Bio,Watermarks,"
+ "RealName,RealNameOverride,IsQuarantined");
+ url.setQuery(q);
+
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("x-xbl-contract-version", "3");
+ request.setRawHeader("Authorization",
+ QString("XBL3.0 x=%1;%2")
+ .arg(m_data->userToken.extra["uhs"].toString(),
+ m_data->xboxApiToken.token)
+ .toUtf8());
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &XboxProfileStep::onRequestDone);
+ requestor->get(request);
+ qDebug() << "Getting Xbox profile...";
+}
+
+void XboxProfileStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to retrieve the Xbox profile."));
+ return;
+ }
+
+#ifndef NDEBUG
+ qDebug() << "XBox profile: " << data;
+#endif
+
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h
new file mode 100644
index 0000000000..cf2c0c3c9b
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class XboxProfileStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit XboxProfileStep(AccountData* data);
+ virtual ~XboxProfileStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp
new file mode 100644
index 0000000000..77afa17fb9
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp
@@ -0,0 +1,93 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#include "XboxUserStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {}
+
+XboxUserStep::~XboxUserStep() noexcept = default;
+
+QString XboxUserStep::describe()
+{
+ return tr("Logging in as an Xbox user.");
+}
+
+void XboxUserStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void XboxUserStep::perform()
+{
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "AuthMethod": "RPS",
+ "SiteName": "user.auth.xboxlive.com",
+ "RpsTicket": "d=%1"
+ },
+ "RelyingParty": "http://auth.xboxlive.com",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
+
+ QNetworkRequest request = QNetworkRequest(
+ QUrl("https://user.auth.xboxlive.com/user/authenticate"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ auto* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &XboxUserStep::onRequestDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "First layer of XBox auth ... commencing.";
+}
+
+void XboxUserStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox user authentication failed."));
+ return;
+ }
+
+ Katabasis::Token temp;
+ if (!Parsers::parseXTokenResponse(data, temp, "UToken")) {
+ qWarning() << "Could not parse user authentication response...";
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox user authentication response could not be understood."));
+ return;
+ }
+ m_data->userToken = temp;
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxUserStep.h b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.h
new file mode 100644
index 0000000000..d783b534c9
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class XboxUserStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit XboxUserStep(AccountData* data);
+ virtual ~XboxUserStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};