diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 2dfc78b5..25639349 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -224,6 +224,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h + minecraft/auth/steps/ForcedMigrationStep.cpp + minecraft/auth/steps/ForcedMigrationStep.h minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 03a68a10..ae88a282 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -239,6 +239,18 @@ void LaunchController::login() { emitFailed(errorString); return; } + case AccountState::MustMigrate: { + auto errorString = tr("The account must be migrated to a Microsoft account."); + QMessageBox::warning( + m_parentWidget, + tr("Account requires migration"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } } } emitFailed(tr("Failed to launch.")); diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 7526c951..b45d1928 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -322,6 +322,7 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { if(type == AccountType::Mojang) { legacy = data.value("legacy").toBool(false); canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); + mustMigrateToMSA = data.value("mustMigrateToMSA").toBool(false); } if(type == AccountType::MSA) { @@ -355,6 +356,9 @@ QJsonObject AccountData::saveState() const { if(canMigrateToMSA) { output["canMigrateToMSA"] = true; } + if(mustMigrateToMSA) { + output["mustMigrateToMSA"] = true; + } } else if (type == AccountType::MSA) { output["type"] = "MSA"; diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index abf84e43..dd328512 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -48,7 +48,8 @@ enum class AccountState { Online, Errored, Expired, - Gone + Gone, + MustMigrate }; struct AccountData { @@ -79,6 +80,7 @@ struct AccountData { AccountType type = AccountType::MSA; bool legacy = false; bool canMigrateToMSA = false; + bool mustMigrateToMSA = false; Katabasis::Token msaToken; Katabasis::Token userToken; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index ef8b435d..90efc3b2 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -294,6 +294,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case AccountState::Gone: { return tr("Gone", "Account status"); } + case AccountState::MustMigrate: { + return tr("Must Migrate", "Account status"); + } } } diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 98d8d94d..e6f1cd38 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -45,6 +45,8 @@ QString AccountTask::getStateMessage() const return tr("Failed to contact the authentication server."); case AccountTaskState::STATE_FAILED_SOFT: return tr("Encountered an error during authentication."); + case AccountTaskState::STATE_FAILED_MUST_MIGRATE: + return tr("Failed to authenticate. The account must be migrated to a Microsoft account to be usable."); case AccountTaskState::STATE_FAILED_HARD: return tr("Failed to authenticate. The session has expired."); case AccountTaskState::STATE_FAILED_GONE: @@ -84,6 +86,12 @@ bool AccountTask::changeState(AccountTaskState newState, QString reason) emitFailed(reason); return false; } + case AccountTaskState::STATE_FAILED_MUST_MIGRATE: { + m_data->errorString = reason; + m_data->accountState = AccountState::MustMigrate; + emitFailed(reason); + return false; + } case AccountTaskState::STATE_FAILED_HARD: { m_data->errorString = reason; m_data->accountState = AccountState::Expired; diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index dac3f1b5..3c1a398a 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -36,6 +36,7 @@ enum class AccountTaskState STATE_WORKING, STATE_SUCCEEDED, STATE_FAILED_SOFT, //!< soft failure. authentication went through partially + STATE_FAILED_MUST_MIGRATE, //!< soft failure. main tokens are valid, but the account must be migrated STATE_FAILED_HARD, //!< hard failure. main tokens are invalid STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ed9e945e..f6909b52 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -145,6 +145,7 @@ void MinecraftAccount::authFailed(QString reason) { switch (m_currentTask->taskState()) { case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_FAILED_MUST_MIGRATE: case AccountTaskState::STATE_FAILED_SOFT: { // NOTE: this doesn't do much. There was an error of some sort. } diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 2dd36562..c2f6478c 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -247,6 +247,36 @@ bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) return true; } +bool parseForcedMigrationResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigrationforced as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigrationforced") { + qWarning() << "Rollout feature is not what we expected (msamigrationforced), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + bool parseRolloutResponse(QByteArray & data, bool& result) { qDebug() << "Parsing Rollout response..."; #ifndef NDEBUG diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h index dac7f69b..db262718 100644 --- a/launcher/minecraft/auth/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -16,4 +16,5 @@ namespace Parsers bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); bool parseRolloutResponse(QByteArray &data, bool& result); + bool parseForcedMigrationResponse(QByteArray & data, bool& result); } diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp index 4661dbe2..c60bb851 100644 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -2,6 +2,7 @@ #include "minecraft/auth/steps/YggdrasilStep.h" #include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/ForcedMigrationStep.h" #include "minecraft/auth/steps/MigrationEligibilityStep.h" #include "minecraft/auth/steps/GetSkinStep.h" @@ -10,6 +11,7 @@ MojangRefresh::MojangRefresh( QObject *parent ) : AuthFlow(data, parent) { m_steps.append(new YggdrasilStep(m_data, QString())); + m_steps.append(new ForcedMigrationStep(m_data)); m_steps.append(new MinecraftProfileStep(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); @@ -21,6 +23,7 @@ MojangLogin::MojangLogin( QObject *parent ): AuthFlow(data, parent), m_password(password) { m_steps.append(new YggdrasilStep(m_data, m_password)); + m_steps.append(new ForcedMigrationStep(m_data)); m_steps.append(new MinecraftProfileStep(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); diff --git a/launcher/minecraft/auth/steps/ForcedMigrationStep.cpp b/launcher/minecraft/auth/steps/ForcedMigrationStep.cpp new file mode 100644 index 00000000..2e816c92 --- /dev/null +++ b/launcher/minecraft/auth/steps/ForcedMigrationStep.cpp @@ -0,0 +1,52 @@ +#include "ForcedMigrationStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +ForcedMigrationStep::ForcedMigrationStep(AccountData* data) : AuthStep(data) { + +} + +ForcedMigrationStep::~ForcedMigrationStep() noexcept = default; + +QString ForcedMigrationStep::describe() { + return tr("Checking for migration eligibility."); +} + +void ForcedMigrationStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigrationforced"); + 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, &ForcedMigrationStep::onRequestDone); + requestor->get(request); +} + +void ForcedMigrationStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void ForcedMigrationStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + Parsers::parseForcedMigrationResponse(data, m_data->mustMigrateToMSA); + } + if(m_data->mustMigrateToMSA) { + emit finished(AccountTaskState::STATE_FAILED_MUST_MIGRATE, tr("The account must be migrated to a Microsoft account.")); + } + else { + emit finished(AccountTaskState::STATE_WORKING, tr("Got forced migration flags")); + } + +} + diff --git a/launcher/minecraft/auth/steps/ForcedMigrationStep.h b/launcher/minecraft/auth/steps/ForcedMigrationStep.h new file mode 100644 index 00000000..8b9cbbbc --- /dev/null +++ b/launcher/minecraft/auth/steps/ForcedMigrationStep.h @@ -0,0 +1,23 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class ForcedMigrationStep : public AuthStep { + Q_OBJECT + +public: + explicit ForcedMigrationStep(AccountData *data); + virtual ~ForcedMigrationStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; +