diff options
Diffstat (limited to 'launcher/minecraft/auth/steps')
20 files changed, 1022 insertions, 0 deletions
diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp new file mode 100644 index 00000000..f726244f --- /dev/null +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -0,0 +1,53 @@ +#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/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h new file mode 100644 index 00000000..9412ae79 --- /dev/null +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -0,0 +1,25 @@ +#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/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp new file mode 100644 index 00000000..3521f8dc --- /dev/null +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -0,0 +1,43 @@ + +#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/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h new file mode 100644 index 00000000..6b97371e --- /dev/null +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -0,0 +1,22 @@ +#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/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp new file mode 100644 index 00000000..c978bd07 --- /dev/null +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -0,0 +1,78 @@ +#include "LauncherLoginStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/AccountTask.h" + +LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { + +} + +LauncherLoginStep::~LauncherLoginStep() noexcept = default; + +QString LauncherLoginStep::describe() { + return tr("Accessing Mojang services."); +} + +void LauncherLoginStep::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, &LauncherLoginStep::onRequestDone); + requestor->post(request, requestBody.toUtf8()); + qDebug() << "Getting Minecraft access token..."; +} + +void LauncherLoginStep::rehydrate() { + // TODO: check the token validity +} + +void LauncherLoginStep::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/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h new file mode 100644 index 00000000..e06a306f --- /dev/null +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class LauncherLoginStep : public AuthStep { + Q_OBJECT + +public: + explicit LauncherLoginStep(AccountData *data); + virtual ~LauncherLoginStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp new file mode 100644 index 00000000..be711f7e --- /dev/null +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -0,0 +1,111 @@ +#include "MSAStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +#include "Application.h" + +using OAuth2 = Katabasis::DeviceFlow; +using Activity = Katabasis::Activity; + +MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) { + OAuth2::Options opts; + opts.scope = "XboxLive.signin offline_access"; + opts.clientIdentifier = APPLICATION->msaClientId(); + opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + // FIXME: OAuth2 is not aware of our fancy shared pointers + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); + + connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); +} + +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: { + m_oauth2->refresh(); + return; + } + case Login: { + QVariantMap extraOpts; + extraOpts["prompt"] = "select_account"; + m_oauth2->setExtraRequestParams(extraOpts); + + *m_data = AccountData(); + m_oauth2->login(); + return; + } + } +} + +void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) { + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::Refreshing: + case Katabasis::Activity::LoggingOut: { + // We asked it to do something, it's doing it. Nothing to act upon. + return; + } + case Katabasis::Activity::Succeeded: { + // Succeeded or did not invalidate tokens + emit hideVerificationUriAndCode(); + QVariantMap extraTokens = m_oauth2->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } +#endif + emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + return; + } + case Katabasis::Activity::FailedSoft: { + // NOTE: soft error in the first step means 'offline' + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error.")); + return; + } + case Katabasis::Activity::FailedGone: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists.")); + return; + } + case Katabasis::Activity::FailedHard: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); + return; + } + default: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); + return; + } + } +} diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h new file mode 100644 index 00000000..49ba3542 --- /dev/null +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -0,0 +1,32 @@ + +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include <katabasis/DeviceFlow.h> + +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 onOAuthActivityChanged(Katabasis::Activity activity); + +private: + Katabasis::DeviceFlow *m_oauth2 = nullptr; + Action m_action; +}; diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp new file mode 100644 index 00000000..f5b5637a --- /dev/null +++ b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp @@ -0,0 +1,45 @@ +#include "MigrationEligibilityStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) { + +} + +MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default; + +QString MigrationEligibilityStep::describe() { + return tr("Checking for migration eligibility."); +} + +void MigrationEligibilityStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); + 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, &MigrationEligibilityStep::onRequestDone); + requestor->get(request); +} + +void MigrationEligibilityStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MigrationEligibilityStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); + } + emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags")); +} diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.h b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h new file mode 100644 index 00000000..b1bf9cbf --- /dev/null +++ b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MigrationEligibilityStep : public AuthStep { + Q_OBJECT + +public: + explicit MigrationEligibilityStep(AccountData *data); + virtual ~MigrationEligibilityStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp new file mode 100644 index 00000000..add91659 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -0,0 +1,91 @@ +#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. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + 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; + } + + if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; + } + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h new file mode 100644 index 00000000..8ef3395c --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -0,0 +1,22 @@ +#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/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp new file mode 100644 index 00000000..07eeb7dc --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -0,0 +1,158 @@ +#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/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h new file mode 100644 index 00000000..31e43bf0 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -0,0 +1,34 @@ +#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/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp new file mode 100644 index 00000000..9f50138e --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -0,0 +1,73 @@ +#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/launcher/minecraft/auth/steps/XboxProfileStep.h b/launcher/minecraft/auth/steps/XboxProfileStep.h new file mode 100644 index 00000000..7a0c5873 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxProfileStep.h @@ -0,0 +1,22 @@ +#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/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp new file mode 100644 index 00000000..a38a28e4 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -0,0 +1,68 @@ +#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/launcher/minecraft/auth/steps/XboxUserStep.h b/launcher/minecraft/auth/steps/XboxUserStep.h new file mode 100644 index 00000000..83e9405f --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxUserStep.h @@ -0,0 +1,22 @@ +#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>); +}; diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp new file mode 100644 index 00000000..4c6b1624 --- /dev/null +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -0,0 +1,51 @@ +#include "YggdrasilStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded); +} + +YggdrasilStep::~YggdrasilStep() noexcept = default; + +QString YggdrasilStep::describe() { + return tr("Logging in with Mojang account."); +} + +void YggdrasilStep::rehydrate() { + // NOOP, for now. +} + +void YggdrasilStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password); + } + else { + m_yggdrasil->refresh(); + } +} + +void YggdrasilStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); +} + +void YggdrasilStep::onAuthFailed() { + // TODO: hook these in again, expand to MSA + // m_error = m_yggdrasil->m_error; + // m_aborted = m_yggdrasil->m_aborted; + + auto state = m_yggdrasil->taskState(); + QString errorMessage = tr("Mojang user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Mojang user authentication ended with a network error."); + } + emit finished(state, errorMessage); +} diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.h b/launcher/minecraft/auth/steps/YggdrasilStep.h new file mode 100644 index 00000000..ebafb8e5 --- /dev/null +++ b/launcher/minecraft/auth/steps/YggdrasilStep.h @@ -0,0 +1,28 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class YggdrasilStep : public AuthStep { + Q_OBJECT + +public: + explicit YggdrasilStep(AccountData *data, QString password); + virtual ~YggdrasilStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onAuthSucceeded(); + void onAuthFailed(); + +private: + Yggdrasil *m_yggdrasil = nullptr; + QString m_password; +}; |