aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/auth')
-rw-r--r--launcher/minecraft/auth/AccountData.h15
-rw-r--r--launcher/minecraft/auth/AccountList.cpp138
-rw-r--r--launcher/minecraft/auth/AccountList.h28
-rw-r--r--launcher/minecraft/auth/AccountTask.cpp75
-rw-r--r--launcher/minecraft/auth/AccountTask.h72
-rw-r--r--launcher/minecraft/auth/AuthRequest.cpp (renamed from launcher/minecraft/auth/flows/AuthRequest.cpp)10
-rw-r--r--launcher/minecraft/auth/AuthRequest.h (renamed from launcher/minecraft/auth/flows/AuthRequest.h)8
-rw-r--r--launcher/minecraft/auth/AuthStep.cpp7
-rw-r--r--launcher/minecraft/auth/AuthStep.h33
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp259
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.h37
-rw-r--r--launcher/minecraft/auth/Parsers.cpp (renamed from launcher/minecraft/auth/flows/Parsers.cpp)2
-rw-r--r--launcher/minecraft/auth/Parsers.h (renamed from launcher/minecraft/auth/flows/Parsers.h)4
-rw-r--r--launcher/minecraft/auth/Yggdrasil.cpp (renamed from launcher/minecraft/auth/flows/Yggdrasil.cpp)34
-rw-r--r--launcher/minecraft/auth/Yggdrasil.h (renamed from launcher/minecraft/auth/flows/Yggdrasil.h)22
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp671
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.h110
-rw-r--r--launcher/minecraft/auth/flows/AuthFlow.cpp71
-rw-r--r--launcher/minecraft/auth/flows/AuthFlow.h45
-rw-r--r--launcher/minecraft/auth/flows/MSA.cpp37
-rw-r--r--launcher/minecraft/auth/flows/MSA.h22
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.cpp22
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.h13
-rw-r--r--launcher/minecraft/auth/flows/MSASilent.cpp16
-rw-r--r--launcher/minecraft/auth/flows/MSASilent.h13
-rw-r--r--launcher/minecraft/auth/flows/Mojang.cpp27
-rw-r--r--launcher/minecraft/auth/flows/Mojang.h26
-rw-r--r--launcher/minecraft/auth/flows/MojangLogin.cpp18
-rw-r--r--launcher/minecraft/auth/flows/MojangLogin.h17
-rw-r--r--launcher/minecraft/auth/flows/MojangRefresh.cpp17
-rw-r--r--launcher/minecraft/auth/flows/MojangRefresh.h10
-rw-r--r--launcher/minecraft/auth/steps/EntitlementsStep.cpp53
-rw-r--r--launcher/minecraft/auth/steps/EntitlementsStep.h25
-rw-r--r--launcher/minecraft/auth/steps/GetSkinStep.cpp43
-rw-r--r--launcher/minecraft/auth/steps/GetSkinStep.h22
-rw-r--r--launcher/minecraft/auth/steps/LauncherLoginStep.cpp78
-rw-r--r--launcher/minecraft/auth/steps/LauncherLoginStep.h22
-rw-r--r--launcher/minecraft/auth/steps/MSAStep.cpp111
-rw-r--r--launcher/minecraft/auth/steps/MSAStep.h32
-rw-r--r--launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp45
-rw-r--r--launcher/minecraft/auth/steps/MigrationEligibilityStep.h22
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStep.cpp83
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStep.h22
-rw-r--r--launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp158
-rw-r--r--launcher/minecraft/auth/steps/XboxAuthorizationStep.h34
-rw-r--r--launcher/minecraft/auth/steps/XboxProfileStep.cpp73
-rw-r--r--launcher/minecraft/auth/steps/XboxProfileStep.h22
-rw-r--r--launcher/minecraft/auth/steps/XboxUserStep.cpp68
-rw-r--r--launcher/minecraft/auth/steps/XboxUserStep.h22
-rw-r--r--launcher/minecraft/auth/steps/YggdrasilStep.cpp51
-rw-r--r--launcher/minecraft/auth/steps/YggdrasilStep.h28
51 files changed, 1665 insertions, 1228 deletions
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h
index 09cd2c73..fa42747e 100644
--- a/launcher/minecraft/auth/AccountData.h
+++ b/launcher/minecraft/auth/AccountData.h
@@ -41,6 +41,16 @@ enum class AccountType {
Mojang
};
+enum class AccountState {
+ Unchecked,
+ Offline,
+ Working,
+ Online,
+ Errored,
+ Expired,
+ Gone
+};
+
struct AccountData {
QJsonObject saveState() const;
bool resumeStateFromV2(QJsonObject data);
@@ -77,4 +87,9 @@ struct AccountData {
MinecraftProfile minecraftProfile;
MinecraftEntitlement minecraftEntitlement;
Katabasis::Validity validity_ = Katabasis::Validity::None;
+
+ // runtime only information (not saved with the account)
+ QString internalId;
+ QString errorString;
+ AccountState accountState = AccountState::Unchecked;
};
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index d7537345..c44e3e89 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -15,6 +15,7 @@
#include "AccountList.h"
#include "AccountData.h"
+#include "AccountTask.h"
#include <QIODevice>
#include <QFile>
@@ -24,6 +25,7 @@
#include <QJsonObject>
#include <QJsonParseError>
#include <QDir>
+#include <QTimer>
#include <QDebug>
@@ -35,7 +37,14 @@ enum AccountListVersion {
MojangMSA = 3
};
-AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { }
+AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) {
+ m_refreshTimer = new QTimer(this);
+ m_refreshTimer->setSingleShot(true);
+ connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue);
+ m_nextTimer = new QTimer(this);
+ m_nextTimer->setSingleShot(true);
+ connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext);
+}
AccountList::~AccountList() noexcept {}
@@ -244,13 +253,29 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
}
case StatusColumn: {
- if(account->isActive()) {
- return tr("Working", "Account status");
- }
- if(account->isExpired()) {
- return tr("Expired", "Account status");
+ switch(account->accountState()) {
+ case AccountState::Unchecked: {
+ return tr("Unchecked", "Account status");
+ }
+ case AccountState::Offline: {
+ return tr("Offline", "Account status");
+ }
+ case AccountState::Online: {
+ return tr("Online", "Account status");
+ }
+ case AccountState::Working: {
+ return tr("Working", "Account status");
+ }
+ case AccountState::Errored: {
+ return tr("Errored", "Account status");
+ }
+ case AccountState::Expired: {
+ return tr("Expired", "Account status");
+ }
+ case AccountState::Gone: {
+ return tr("Gone", "Account status");
+ }
}
- return tr("Ready", "Account status");
}
case ProfileNameColumn: {
@@ -583,10 +608,105 @@ void AccountList::setListFilePath(QString path, bool autosave)
bool AccountList::anyAccountIsValid()
{
- for(auto account:m_accounts)
+ for(auto account: m_accounts)
{
- if(account->accountStatus() != NotVerified)
+ if(account->ownsMinecraft()) {
return true;
+ }
}
return false;
}
+
+void AccountList::fillQueue() {
+
+ if(m_defaultAccount && m_defaultAccount->shouldRefresh()) {
+ auto idToRefresh = m_defaultAccount->internalId();
+ m_refreshQueue.push_back(idToRefresh);
+ qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first";
+ }
+
+ for(int i = 0; i < count(); i++) {
+ auto account = at(i);
+ if(account == m_defaultAccount) {
+ continue;
+ }
+
+ if(account->shouldRefresh()) {
+ auto idToRefresh = account->internalId();
+ m_refreshQueue.push_back(idToRefresh);
+ qDebug() << "AccountList: Queued account with internal ID " << idToRefresh << " to refresh";
+ }
+ }
+ m_refreshQueue.removeDuplicates();
+ tryNext();
+}
+
+void AccountList::requestRefresh(QString accountId) {
+ m_refreshQueue.push_back(accountId);
+ if(!isActive()) {
+ tryNext();
+ }
+}
+
+void AccountList::tryNext() {
+ beginActivity();
+ while (m_refreshQueue.length()) {
+ auto accountId = m_refreshQueue.front();
+ m_refreshQueue.pop_front();
+ for(int i = 0; i < count(); i++) {
+ auto account = at(i);
+ if(account->internalId() == accountId) {
+ m_currentTask = account->refresh();
+ if(m_currentTask) {
+ connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded);
+ connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed);
+ m_currentTask->start();
+ qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId;
+ return;
+ }
+ }
+ }
+ qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found.";
+ }
+ endActivity();
+ // if we get here, no account needed refreshing. Schedule refresh in an hour.
+ m_refreshTimer->start(std::chrono::hours(1));
+}
+
+void AccountList::authSucceeded() {
+ qDebug() << "RefreshSchedule: Background account refresh succeeded";
+ m_currentTask.reset();
+ endActivity();
+ m_nextTimer->start(std::chrono::seconds(20));
+}
+
+void AccountList::authFailed(QString reason) {
+ qDebug() << "RefreshSchedule: Background account refresh failed: " << reason;
+ m_currentTask.reset();
+ endActivity();
+ m_nextTimer->start(std::chrono::seconds(20));
+}
+
+bool AccountList::isActive() const {
+ return m_activityCount != 0;
+}
+
+void AccountList::beginActivity() {
+ bool activating = m_activityCount == 0;
+ m_activityCount++;
+ if(activating) {
+ emit activityChanged(true);
+ }
+}
+
+void AccountList::endActivity() {
+ if(m_activityCount == 0) {
+ qWarning() << m_name << " - Activity count would become below zero";
+ return;
+ }
+ bool deactivating = m_activityCount == 1;
+ m_activityCount--;
+ if(deactivating) {
+ emit activityChanged(false);
+ }
+}
diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h
index 08004628..75686c21 100644
--- a/launcher/minecraft/auth/AccountList.h
+++ b/launcher/minecraft/auth/AccountList.h
@@ -67,6 +67,8 @@ public:
MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const;
QStringList profileNames() const;
+ void requestRefresh(QString accountId);
+
/*!
* Sets the path to load/save the list file from/to.
* If autosave is true, this list will automatically save to the given path whenever it changes.
@@ -85,10 +87,20 @@ public:
void setDefaultAccount(MinecraftAccountPtr profileId);
bool anyAccountIsValid();
+ bool isActive() const;
+
+protected:
+ void beginActivity();
+ void endActivity();
+
+private:
+ const char* m_name;
+ uint32_t m_activityCount = 0;
signals:
void listChanged();
void listActivityChanged();
void defaultAccountChanged();
+ void activityChanged(bool active);
public slots:
/**
@@ -101,7 +113,23 @@ public slots:
*/
void accountActivityChanged(bool active);
+ /**
+ * This is initially to run background account refresh tasks, or on a hourly timer
+ */
+ void fillQueue();
+
+private slots:
+ void tryNext();
+
+ void authSucceeded();
+ void authFailed(QString reason);
+
protected:
+ QList<QString> m_refreshQueue;
+ QTimer *m_refreshTimer;
+ QTimer *m_nextTimer;
+ shared_qobject_ptr<AccountTask> m_currentTask;
+
/*!
* Called whenever the list changes.
* This emits the listChanged() signal and autosaves the list (if autosave is enabled).
diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp
index 25d753de..98d8d94d 100644
--- a/launcher/minecraft/auth/AccountTask.cpp
+++ b/launcher/minecraft/auth/AccountTask.cpp
@@ -28,40 +28,79 @@
AccountTask::AccountTask(AccountData *data, QObject *parent)
: Task(parent), m_data(data)
{
- changeState(STATE_CREATED);
+ changeState(AccountTaskState::STATE_CREATED);
}
QString AccountTask::getStateMessage() const
{
- switch (m_accountState)
+ switch (m_taskState)
{
- case STATE_CREATED:
+ case AccountTaskState::STATE_CREATED:
return "Waiting...";
- case STATE_WORKING:
+ case AccountTaskState::STATE_WORKING:
return tr("Sending request to auth servers...");
- case STATE_SUCCEEDED:
+ case AccountTaskState::STATE_SUCCEEDED:
return tr("Authentication task succeeded.");
- case STATE_FAILED_SOFT:
+ case AccountTaskState::STATE_OFFLINE:
return tr("Failed to contact the authentication server.");
- case STATE_FAILED_HARD:
- return tr("Failed to authenticate.");
- case STATE_FAILED_GONE:
+ case AccountTaskState::STATE_FAILED_SOFT:
+ return tr("Encountered an error during authentication.");
+ case AccountTaskState::STATE_FAILED_HARD:
+ return tr("Failed to authenticate. The session has expired.");
+ case AccountTaskState::STATE_FAILED_GONE:
return tr("Failed to authenticate. The account no longer exists.");
default:
return tr("...");
}
}
-void AccountTask::changeState(AccountTask::State newState, QString reason)
+bool AccountTask::changeState(AccountTaskState newState, QString reason)
{
- m_accountState = newState;
+ m_taskState = newState;
setStatus(getStateMessage());
- if (newState == STATE_SUCCEEDED)
- {
- emitSucceeded();
- }
- else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE)
- {
- emitFailed(reason);
+ switch(newState) {
+ case AccountTaskState::STATE_CREATED: {
+ m_data->errorString.clear();
+ return true;
+ }
+ case AccountTaskState::STATE_WORKING: {
+ m_data->accountState = AccountState::Working;
+ return true;
+ }
+ case AccountTaskState::STATE_SUCCEEDED: {
+ m_data->accountState = AccountState::Online;
+ emitSucceeded();
+ return false;
+ }
+ case AccountTaskState::STATE_OFFLINE: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Offline;
+ emitFailed(reason);
+ return false;
+ }
+ case AccountTaskState::STATE_FAILED_SOFT: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Errored;
+ emitFailed(reason);
+ return false;
+ }
+ case AccountTaskState::STATE_FAILED_HARD: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Expired;
+ emitFailed(reason);
+ return false;
+ }
+ case AccountTaskState::STATE_FAILED_GONE: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Gone;
+ emitFailed(reason);
+ return false;
+ }
+ default: {
+ QString error = tr("Unknown account task state: %1").arg(int(newState));
+ m_data->accountState = AccountState::Errored;
+ emitFailed(error);
+ return false;
+ }
}
}
diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h
index 4f3bd52a..dac3f1b5 100644
--- a/launcher/minecraft/auth/AccountTask.h
+++ b/launcher/minecraft/auth/AccountTask.h
@@ -26,62 +26,32 @@
class QNetworkReply;
+/**
+ * Enum for describing the state of the current task.
+ * Used by the getStateMessage function to determine what the status message should be.
+ */
+enum class AccountTaskState
+{
+ STATE_CREATED,
+ STATE_WORKING,
+ STATE_SUCCEEDED,
+ STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
+ 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
+};
+
class AccountTask : public Task
{
- friend class AuthContext;
Q_OBJECT
public:
explicit AccountTask(AccountData * data, QObject *parent = 0);
virtual ~AccountTask() {};
- /**
- * assign a session to this task. the session will be filled with required infomration
- * upon completion
- */
- void assignSession(AuthSessionPtr session)
- {
- m_session = session;
- }
-
- /// get the assigned session for filling with information.
- AuthSessionPtr getAssignedSession()
- {
- return m_session;
- }
-
- /**
- * Class describing a Account error response.
- */
- struct Error
- {
- QString m_errorMessageShort;
- QString m_errorMessageVerbose;
- QString m_cause;
- };
-
- enum AbortedBy
- {
- BY_NOTHING,
- BY_USER,
- BY_TIMEOUT
- } m_aborted = BY_NOTHING;
-
- /**
- * Enum for describing the state of the current task.
- * Used by the getStateMessage function to determine what the status message should be.
- */
- enum State
- {
- STATE_CREATED,
- STATE_WORKING,
- STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
- STATE_FAILED_HARD, //!< hard failure. auth is invalid
- STATE_FAILED_GONE, //!< hard failure. auth is invalid, and the account no longer exists
- STATE_SUCCEEDED
- } m_accountState = STATE_CREATED;
+ AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
- State accountState() {
- return m_accountState;
+ AccountTaskState taskState() {
+ return m_taskState;
}
signals:
@@ -98,11 +68,9 @@ protected:
virtual QString getStateMessage() const;
protected slots:
- void changeState(State newState, QString reason=QString());
+ // NOTE: true -> non-terminal state, false -> terminal state
+ bool changeState(AccountTaskState newState, QString reason = QString());
protected:
- // FIXME: segfault disaster waiting to happen
AccountData *m_data = nullptr;
- std::shared_ptr<Error> m_error;
- AuthSessionPtr m_session;
};
diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp
index 82dba591..459d2354 100644
--- a/launcher/minecraft/auth/flows/AuthRequest.cpp
+++ b/launcher/minecraft/auth/AuthRequest.cpp
@@ -44,6 +44,7 @@ void AuthRequest::onRequestFinished() {
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
return;
}
+ httpStatus_ = 200;
finish();
}
@@ -55,10 +56,11 @@ void AuthRequest::onRequestError(QNetworkReply::NetworkError error) {
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
return;
}
- qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString();
- int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+ errorString_ = reply_->errorString();
+ httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
error_ = error;
+ qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_;
+ qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
// QTimer::singleShot(10, this, SLOT(finish()));
}
@@ -103,6 +105,8 @@ void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Opera
status_ = Requesting;
error_ = QNetworkReply::NoError;
+ errorString_.clear();
+ httpStatus_ = 0;
}
void AuthRequest::finish() {
diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/AuthRequest.h
index a547aea4..89f7a123 100644
--- a/launcher/minecraft/auth/flows/AuthRequest.h
+++ b/launcher/minecraft/auth/AuthRequest.h
@@ -46,6 +46,11 @@ protected slots:
/// Handle upload progress.
void onUploadProgress(qint64 uploaded, qint64 total);
+public:
+ QNetworkReply::NetworkError error_;
+ int httpStatus_ = 0;
+ QString errorString_;
+
protected:
void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray());
@@ -60,5 +65,6 @@ protected:
QNetworkAccessManager::Operation operation_;
QUrl url_;
Katabasis::ReplyList timedReplies_;
- QNetworkReply::NetworkError error_;
+
+ QTimer *timer_;
};
diff --git a/launcher/minecraft/auth/AuthStep.cpp b/launcher/minecraft/auth/AuthStep.cpp
new file mode 100644
index 00000000..ffa2581b
--- /dev/null
+++ b/launcher/minecraft/auth/AuthStep.cpp
@@ -0,0 +1,7 @@
+#include "AuthStep.h"
+
+AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) {
+}
+
+AuthStep::~AuthStep() noexcept = default;
+
diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h
new file mode 100644
index 00000000..2a8dc2ca
--- /dev/null
+++ b/launcher/minecraft/auth/AuthStep.h
@@ -0,0 +1,33 @@
+#pragma once
+#include <QObject>
+#include <QList>
+#include <QNetworkReply>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AccountData.h"
+#include "AccountTask.h"
+
+class AuthStep : public QObject {
+ Q_OBJECT
+
+public:
+ using Ptr = shared_qobject_ptr<AuthStep>;
+
+public:
+ explicit AuthStep(AccountData *data);
+ virtual ~AuthStep() noexcept;
+
+ virtual QString describe() = 0;
+
+public slots:
+ virtual void perform() = 0;
+ virtual void rehydrate() = 0;
+
+signals:
+ void finished(AccountTaskState resultingState, QString message);
+ void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
+ void hideVerificationUriAndCode();
+
+protected:
+ AccountData *m_data;
+};
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 30ed6afe..7ce87a3d 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -16,7 +16,6 @@
*/
#include "MinecraftAccount.h"
-#include "flows/AuthContext.h"
#include <QUuid>
#include <QJsonObject>
@@ -28,14 +27,12 @@
#include <QDebug>
#include <QPainter>
-#include "flows/MSASilent.h"
-#include "flows/MSAInteractive.h"
-#include "flows/MojangRefresh.h"
-#include "flows/MojangLogin.h"
+#include "flows/MSA.h"
+#include "flows/Mojang.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
- m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
+ data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
}
@@ -77,42 +74,10 @@ QJsonObject MinecraftAccount::saveToJson() const
return data.saveState();
}
-AccountStatus MinecraftAccount::accountStatus() const {
- if(data.type == AccountType::Mojang) {
- if (data.accessToken().isEmpty()) {
- return NotVerified;
- }
- else {
- return Verified;
- }
- }
- // MSA
- // FIXME: this is extremely crude and probably wrong
- if(data.msaToken.token.isEmpty()) {
- return NotVerified;
- }
- else {
- return Verified;
- }
-}
-
-bool MinecraftAccount::isExpired() const {
- switch(data.type) {
- case AccountType::Mojang: {
- return data.accessToken().isEmpty();
- }
- break;
- case AccountType::MSA: {
- return data.msaToken.validity == Katabasis::Validity::None;
- }
- break;
- default: {
- return true;
- }
- }
+AccountState MinecraftAccount::accountState() const {
+ return data.accountState;
}
-
QPixmap MinecraftAccount::getFace() const {
QPixmap skinTexture;
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
@@ -126,136 +91,51 @@ QPixmap MinecraftAccount::getFace() const {
}
-shared_qobject_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password)
-{
+shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr);
- // take care of the true offline status
- if (accountStatus() == NotVerified && password.isEmpty())
- {
- if (session)
- {
- session->status = AuthSession::RequiresPassword;
- fillSession(session);
- }
- return nullptr;
- }
-
- if(accountStatus() == Verified && !session->wants_online)
- {
- session->status = AuthSession::PlayableOffline;
- session->auth_server_online = false;
- fillSession(session);
- return nullptr;
- }
- else
- {
- if (password.isEmpty())
- {
- m_currentTask.reset(new MojangRefresh(&data));
- }
- else
- {
- m_currentTask.reset(new MojangLogin(&data, password));
- }
- m_currentTask->assignSession(session);
-
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
- emit activityChanged(true);
- }
+ m_currentTask.reset(new MojangLogin(&data, password));
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ emit activityChanged(true);
return m_currentTask;
}
-shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) {
+shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() {
Q_ASSERT(m_currentTask.get() == nullptr);
- if(accountStatus() == Verified && !session->wants_online)
- {
- session->status = AuthSession::PlayableOffline;
- session->auth_server_online = false;
- fillSession(session);
- return nullptr;
- }
- else
- {
- m_currentTask.reset(new MSAInteractive(&data));
- m_currentTask->assignSession(session);
-
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
- emit activityChanged(true);
- }
+ m_currentTask.reset(new MSAInteractive(&data));
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ emit activityChanged(true);
return m_currentTask;
}
-shared_qobject_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) {
- Q_ASSERT(m_currentTask.get() == nullptr);
-
- // take care of the true offline status
- if (accountStatus() == NotVerified)
- {
- if (session)
- {
- if(data.type == AccountType::MSA) {
- session->status = AuthSession::RequiresOAuth;
- }
- else {
- session->status = AuthSession::RequiresPassword;
- }
- fillSession(session);
- }
- return nullptr;
+shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
+ if(m_currentTask) {
+ return m_currentTask;
}
- if(accountStatus() == Verified && !session->wants_online)
- {
- session->status = AuthSession::PlayableOffline;
- session->auth_server_online = false;
- fillSession(session);
- return nullptr;
+ if(data.type == AccountType::MSA) {
+ m_currentTask.reset(new MSASilent(&data));
}
- else
- {
- if(data.type == AccountType::MSA) {
- m_currentTask.reset(new MSASilent(&data));
- }
- else {
- m_currentTask.reset(new MojangRefresh(&data));
- }
- m_currentTask->assignSession(session);
-
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
- emit activityChanged(true);
+ else {
+ m_currentTask.reset(new MojangRefresh(&data));
}
+
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ emit activityChanged(true);
+ return m_currentTask;
+}
+
+shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask() {
return m_currentTask;
}
void MinecraftAccount::authSucceeded()
{
- auto session = m_currentTask->getAssignedSession();
- if (session)
- {
- /*
- session->status = AuthSession::RequiresProfileSetup;
- session->auth_server_online = true;
- */
- if(data.profileId().size() == 0) {
- session->status = AuthSession::RequiresProfileSetup;
- }
- else {
- if(session->wants_online) {
- session->status = AuthSession::PlayableOnline;
- }
- else {
- session->status = AuthSession::PlayableOffline;
- }
- }
- fillSession(session);
- session->auth_server_online = true;
- }
m_currentTask.reset();
emit changed();
emit activityChanged(false);
@@ -263,62 +143,35 @@ void MinecraftAccount::authSucceeded()
void MinecraftAccount::authFailed(QString reason)
{
- auto session = m_currentTask->getAssignedSession();
- // This is emitted when the yggdrasil tasks time out or are cancelled.
- // -> we treat the error as no-op
- switch (m_currentTask->accountState()) {
- case AccountTask::STATE_FAILED_SOFT: {
- if (session)
- {
- if(accountStatus() == Verified) {
- session->status = AuthSession::PlayableOffline;
- }
- else {
- if(data.type == AccountType::MSA) {
- session->status = AuthSession::RequiresOAuth;
- }
- else {
- session->status = AuthSession::RequiresPassword;
- }
- }
- session->auth_server_online = false;
- fillSession(session);
- }
+ switch (m_currentTask->taskState()) {
+ case AccountTaskState::STATE_OFFLINE:
+ case AccountTaskState::STATE_FAILED_SOFT: {
+ // NOTE: this doesn't do much. There was an error of some sort.
}
break;
- case AccountTask::STATE_FAILED_HARD: {
- // FIXME: MSA data clearing
- data.yggdrasilToken.token = QString();
- data.yggdrasilToken.validity = Katabasis::Validity::None;
- data.validity_ = Katabasis::Validity::None;
- emit changed();
- if (session)
- {
- if(data.type == AccountType::MSA) {
- session->status = AuthSession::RequiresOAuth;
- }
- else {
- session->status = AuthSession::RequiresPassword;
- }
- session->auth_server_online = true;
- fillSession(session);
+ case AccountTaskState::STATE_FAILED_HARD: {
+ if(isMSA()) {
+ data.msaToken.token = QString();
+ data.msaToken.refresh_token = QString();
+ data.msaToken.validity = Katabasis::Validity::None;
+ data.validity_ = Katabasis::Validity::None;
+ }
+ else {
+ data.yggdrasilToken.token = QString();
+ data.yggdrasilToken.validity = Katabasis::Validity::None;
+ data.validity_ = Katabasis::Validity::None;
}
+ emit changed();
}
break;
- case AccountTask::STATE_FAILED_GONE: {
+ case AccountTaskState::STATE_FAILED_GONE: {
data.validity_ = Katabasis::Validity::None;
emit changed();
- if (session)
- {
- session->status = AuthSession::GoneOrMigrated;
- session->auth_server_online = true;
- fillSession(session);
- }
}
break;
- case AccountTask::STATE_CREATED:
- case AccountTask::STATE_WORKING:
- case AccountTask::STATE_SUCCEEDED: {
+ case AccountTaskState::STATE_CREATED:
+ case AccountTaskState::STATE_WORKING:
+ case AccountTaskState::STATE_SUCCEEDED: {
// Not reachable here, as they are not failures.
}
}
@@ -366,6 +219,18 @@ bool MinecraftAccount::shouldRefresh() const {
void MinecraftAccount::fillSession(AuthSessionPtr session)
{
+ if(ownsMinecraft() && !hasProfile()) {
+ session->status = AuthSession::RequiresProfileSetup;
+ }
+ else {
+ if(session->wants_online) {
+ session->status = AuthSession::PlayableOnline;
+ }
+ else {
+ session->status = AuthSession::PlayableOffline;
+ }
+ }
+
// the user name. you have to have an user name
// FIXME: not with MSA
session->username = data.userName();
diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index 459ef903..18f142c4 100644
--- a/launcher/minecraft/auth/MinecraftAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -24,6 +24,7 @@
#include <QPixmap>
#include <memory>
+
#include "AuthSession.h"
#include "Usable.h"
#include "AccountData.h"
@@ -50,12 +51,6 @@ struct AccountProfile
bool legacy;
};
-enum AccountStatus
-{
- NotVerified,
- Verified
-};
-
/**
* Object that stores information about a certain Mojang account.
*
@@ -90,15 +85,17 @@ public: /* manipulation */
* Attempt to login. Empty password means we use the token.
* If the attempt fails because we already are performing some task, it returns false.
*/
- shared_qobject_ptr<AccountTask> login(AuthSessionPtr session, QString password);
+ shared_qobject_ptr<AccountTask> login(QString password);
- shared_qobject_ptr<AccountTask> loginMSA(AuthSessionPtr session);
+ shared_qobject_ptr<AccountTask> loginMSA();
- shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session);
+ shared_qobject_ptr<AccountTask> refresh();
+
+ shared_qobject_ptr<AccountTask> currentTask();
public: /* queries */
QString internalId() const {
- return m_internalId;
+ return data.internalId;
}
QString accountDisplayString() const {
@@ -123,8 +120,6 @@ public: /* queries */
bool isActive() const;
- bool isExpired() const;
-
bool canMigrate() const {
return data.canMigrateToMSA;
}
@@ -133,6 +128,14 @@ public: /* queries */
return data.type == AccountType::MSA;
}
+ bool ownsMinecraft() const {
+ return data.minecraftEntitlement.ownsMinecraft;
+ }
+
+ bool hasProfile() const {
+ return data.profileId().size() != 0;
+ }
+
QString typeString() const {
switch(data.type) {
case AccountType::Mojang: {
@@ -154,8 +157,8 @@ public: /* queries */
QPixmap getFace() const;
- //! Returns whether the account is NotVerified, Verified or Online
- AccountStatus accountStatus() const;
+ //! Returns the current state of the account
+ AccountState accountState() const;
AccountData * accountData() {
return &data;
@@ -163,6 +166,8 @@ public: /* queries */
bool shouldRefresh() const;
+ void fillSession(AuthSessionPtr session);
+
signals:
/**
* This signal is emitted when the account changes
@@ -174,7 +179,6 @@ signals:
// TODO: better signalling for the various possible state changes - especially errors
protected: /* variables */
- QString m_internalId;
AccountData data;
// current task we are executing here
@@ -189,7 +193,4 @@ private
slots:
void authSucceeded();
void authFailed(QString reason);
-
-private:
- void fillSession(AuthSessionPtr session);
};
diff --git a/launcher/minecraft/auth/flows/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp
index ecb11cf9..4cab78ef 100644
--- a/launcher/minecraft/auth/flows/Parsers.cpp
+++ b/launcher/minecraft/auth/Parsers.cpp
@@ -72,7 +72,7 @@ bool getBool(QJsonValue value, bool & out) {
// 2148916238 = child account not linked to a family
*/
-bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) {
+bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) {
qDebug() << "Parsing" << name <<":";
#ifndef NDEBUG
qDebug() << data;
diff --git a/launcher/minecraft/auth/flows/Parsers.h b/launcher/minecraft/auth/Parsers.h
index b484a073..dac7f69b 100644
--- a/launcher/minecraft/auth/flows/Parsers.h
+++ b/launcher/minecraft/auth/Parsers.h
@@ -1,6 +1,6 @@
#pragma once
-#include "../AccountData.h"
+#include "AccountData.h"
namespace Parsers
{
@@ -10,7 +10,7 @@ namespace Parsers
bool getNumber(QJsonValue value, int64_t & out);
bool getBool(QJsonValue value, bool & out);
- bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name);
+ bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, QString name);
bool parseMojangResponse(QByteArray &data, Katabasis::Token &output);
bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output);
diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp
index 5ea168e8..7ac842a6 100644
--- a/launcher/minecraft/auth/flows/Yggdrasil.cpp
+++ b/launcher/minecraft/auth/Yggdrasil.cpp
@@ -14,7 +14,7 @@
*/
#include "Yggdrasil.h"
-#include "../AccountData.h"
+#include "AccountData.h"
#include <QObject>
#include <QString>
@@ -30,11 +30,11 @@
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
: AccountTask(data, parent)
{
- changeState(STATE_CREATED);
+ changeState(AccountTaskState::STATE_CREATED);
}
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
- changeState(STATE_WORKING);
+ changeState(AccountTaskState::STATE_WORKING);
QNetworkRequest netRequest(endpoint);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
@@ -185,14 +185,14 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
QString clientToken = responseData.value("clientToken").toString("");
if (clientToken.isEmpty()) {
// Fail if the server gave us an empty client token
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
+ changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
return;
}
if(m_data->clientToken().isEmpty()) {
m_data->setClientToken(clientToken);
}
else if(clientToken != m_data->clientToken()) {
- changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
+ changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
return;
}
@@ -201,7 +201,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
QString accessToken = responseData.value("accessToken").toString("");
if (accessToken.isEmpty()) {
// Fail if the server didn't give us an access token.
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
+ changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
return;
}
// Set the access token.
@@ -212,25 +212,25 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
qDebug() << "Finished reading authentication response.";
- changeState(STATE_SUCCEEDED);
+ changeState(AccountTaskState::STATE_SUCCEEDED);
}
void Yggdrasil::processReply() {
- changeState(STATE_WORKING);
+ changeState(AccountTaskState::STATE_WORKING);
switch (m_netReply->error())
{
case QNetworkReply::NoError:
break;
case QNetworkReply::TimeoutError:
- changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
+ changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
return;
case QNetworkReply::OperationCanceledError:
- changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
+ changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
return;
case QNetworkReply::SslHandshakeFailedError:
changeState(
- STATE_FAILED_SOFT,
+ AccountTaskState::STATE_FAILED_SOFT,
tr(
"<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
@@ -248,13 +248,13 @@ void Yggdrasil::processReply() {
break;
case QNetworkReply::ContentGoneError: {
changeState(
- STATE_FAILED_GONE,
+ AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
);
}
default:
changeState(
- STATE_FAILED_SOFT,
+ AccountTaskState::STATE_FAILED_SOFT,
tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error())
);
return;
@@ -279,7 +279,7 @@ void Yggdrasil::processReply() {
}
else {
changeState(
- STATE_FAILED_SOFT,
+ AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)
);
qCritical() << replyData;
@@ -303,7 +303,7 @@ void Yggdrasil::processReply() {
// error.
qDebug() << "The request failed and the server gave no error message. Unknown error.";
changeState(
- STATE_FAILED_SOFT,
+ AccountTaskState::STATE_FAILED_SOFT,
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString())
);
}
@@ -322,10 +322,10 @@ void Yggdrasil::processError(QJsonObject responseData) {
causeVal.toString("")
}
);
- changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
+ changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
}
else {
// Error is not in standard format. Don't set m_error and return unknown error.
- changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
+ changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
}
}
diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h
index b9670ec7..4f52a04c 100644
--- a/launcher/minecraft/auth/flows/Yggdrasil.h
+++ b/launcher/minecraft/auth/Yggdrasil.h
@@ -15,14 +15,14 @@
#pragma once
-#include "../AccountTask.h"
+#include "AccountTask.h"
#include <QString>
#include <QJsonObject>
#include <QTimer>
#include <qsslerror.h>
-#include "../MinecraftAccount.h"
+#include "MinecraftAccount.h"
class QNetworkAccessManager;
class QNetworkReply;
@@ -38,10 +38,26 @@ public:
AccountData *data,
QObject *parent = 0
);
- virtual ~Yggdrasil() {};
+ virtual ~Yggdrasil() = default;
void refresh();
void login(QString password);
+
+ struct Error
+ {
+ QString m_errorMessageShort;
+ QString m_errorMessageVerbose;
+ QString m_cause;
+ };
+ std::shared_ptr<Error> m_error;
+
+ enum AbortedBy
+ {
+ BY_NOTHING,
+ BY_USER,
+ BY_TIMEOUT
+ } m_aborted = BY_NOTHING;
+
protected:
void executeTask() override;
diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp
deleted file mode 100644
index 00957fd4..00000000
--- a/launcher/minecraft/auth/flows/AuthContext.cpp
+++ /dev/null
@@ -1,671 +0,0 @@
-#include <QNetworkAccessManager>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QDesktopServices>
-#include <QMetaEnum>
-#include <QDebug>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QUuid>
-#include <QUrlQuery>
-
-#include "AuthContext.h"
-#include "katabasis/Globals.h"
-#include "AuthRequest.h"
-
-#include "Parsers.h"
-
-#include <Application.h>
-
-using OAuth2 = Katabasis::DeviceFlow;
-using Activity = Katabasis::Activity;
-
-AuthContext::AuthContext(AccountData * data, QObject *parent) :
- AccountTask(data, parent)
-{
-}
-
-void AuthContext::beginActivity(Activity activity) {
- if(isBusy()) {
- throw 0;
- }
- m_activity = activity;
- changeState(STATE_WORKING, "Initializing");
- emit activityChanged(m_activity);
-}
-
-void AuthContext::finishActivity() {
- if(!isBusy()) {
- throw 0;
- }
- m_activity = Katabasis::Activity::Idle;
- setStage(AuthStage::Complete);
- m_data->validity_ = m_data->minecraftProfile.validity;
- emit activityChanged(m_activity);
-}
-
-void AuthContext::initMSA() {
- if(m_oauth2) {
- return;
- }
-
- 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, &AuthContext::onOAuthActivityChanged);
- connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
-}
-
-void AuthContext::initMojang() {
- if(m_yggdrasil) {
- return;
- }
- m_yggdrasil = new Yggdrasil(m_data, this);
-
- connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
- connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
-}
-
-void AuthContext::onMojangSucceeded() {
- doMinecraftProfile();
-}
-
-
-void AuthContext::onMojangFailed() {
- finishActivity();
- m_error = m_yggdrasil->m_error;
- m_aborted = m_yggdrasil->m_aborted;
- changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed."));
-}
-
-void AuthContext::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();
- if (!m_oauth2->linked()) {
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
- return;
- }
- 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
- doUserAuth();
- return;
- }
- case Katabasis::Activity::FailedSoft: {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error."));
- return;
- }
- case Katabasis::Activity::FailedGone:
- case Katabasis::Activity::FailedHard: {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
- return;
- }
- default: {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
- return;
- }
-
- }
-}
-
-void AuthContext::doUserAuth() {
- setStage(AuthStage::UserAuth);
- changeState(STATE_WORKING, tr("Starting user authentication"));
-
- 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, &AuthContext::onUserAuthDone);
- requestor->post(request, xbox_auth_data.toUtf8());
- qDebug() << "First layer of XBox auth ... commencing.";
-}
-
-void AuthContext::onUserAuthDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("XBox user authentication failed."));
- return;
- }
-
- Katabasis::Token temp;
- if(!Parsers::parseXTokenResponse(replyData, temp, "UToken")) {
- qWarning() << "Could not parse user authentication response...";
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood."));
- return;
- }
- m_data->userToken = temp;
-
- setStage(AuthStage::XboxAuth);
- changeState(STATE_WORKING, tr("Starting XBox authentication"));
-
- doSTSAuthMinecraft();
- doSTSAuthGeneric();
-}
-/*
- url = "https://xsts.auth.xboxlive.com/xsts/authorize"
- headers = {"x-xbl-contract-version": "1"}
- data = {
- "RelyingParty": relying_party,
- "TokenType": "JWT",
- "Properties": {
- "UserTokens": [self.user_token.token],
- "SandboxId": "RETAIL",
- },
- }
-*/
-void AuthContext::doSTSAuthMinecraft() {
- QString xbox_auth_template = R"XXX(
-{
- "Properties": {
- "SandboxId": "RETAIL",
- "UserTokens": [
- "%1"
- ]
- },
- "RelyingParty": "rp://api.minecraftservices.com/",
- "TokenType": "JWT"
-}
-)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
-
- 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, &AuthContext::onSTSAuthMinecraftDone);
- requestor->post(request, xbox_auth_data.toUtf8());
- qDebug() << "Getting Minecraft services STS token...";
-}
-
-void AuthContext::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();
- return;
- }
-
- int64_t errorCode = -1;
- auto obj = doc.object();
- if(!Parsers::getNumber(obj.value("XErr"), errorCode)) {
- qWarning() << "XErr is not a number";
- return;
- }
- stsErrors.insert(errorCode);
- stsFailed = true;
- }
-}
-
-
-void AuthContext::onSTSAuthMinecraftDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- processSTSError(error, replyData, headers);
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- Katabasis::Token temp;
- if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) {
- qWarning() << "Could not parse authorization response for access to mojang services...";
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
- qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
- failResult(m_mcAuthSucceeded);
- return;
- }
- m_data->mojangservicesToken = temp;
-
- doMinecraftAuth();
-}
-
-void AuthContext::doMinecraftAuth() {
- 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, &AuthContext::onMinecraftAuthDone);
- requestor->post(request, requestBody.toUtf8());
- qDebug() << "Getting Minecraft access token...";
-}
-
-void AuthContext::onMinecraftAuthDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- qDebug() << replyData;
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) {
- qWarning() << "Could not parse login_with_xbox response...";
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- succeedResult(m_mcAuthSucceeded);
-}
-
-void AuthContext::doSTSAuthGeneric() {
- QString xbox_auth_template = R"XXX(
-{
- "Properties": {
- "SandboxId": "RETAIL",
- "UserTokens": [
- "%1"
- ]
- },
- "RelyingParty": "http://xboxlive.com",
- "TokenType": "JWT"
-}
-)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
-
- 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, &AuthContext::onSTSAuthGenericDone);
- requestor->post(request, xbox_auth_data.toUtf8());
- qDebug() << "Getting generic STS token...";
-}
-
-void AuthContext::onSTSAuthGenericDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- processSTSError(error, replyData, headers);
- failResult(m_xboxProfileSucceeded);
- return;
- }
-
- Katabasis::Token temp;
- if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) {
- qWarning() << "Could not parse authorization response for access to xbox API...";
- failResult(m_xboxProfileSucceeded);
- return;
- }
-
- if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
- qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
- failResult(m_xboxProfileSucceeded);
- return;
- }
- m_data->xboxApiToken = temp;
-
- doXBoxProfile();
-}
-
-void AuthContext::doXBoxProfile() {
- 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, &AuthContext::onXBoxProfileDone);
- requestor->get(request);
- qDebug() << "Getting Xbox profile...";
-}
-
-void AuthContext::onXBoxProfileDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- failResult(m_xboxProfileSucceeded);
- return;
- }
-
-#ifndef NDEBUG
- qDebug() << "XBox profile: " << replyData;
-#endif
-
- succeedResult(m_xboxProfileSucceeded);
-}
-
-void AuthContext::succeedResult(bool& flag) {
- m_requestsDone ++;
- flag = true;
- checkResult();
-}
-
-void AuthContext::failResult(bool& flag) {
- m_requestsDone ++;
- flag = false;
- checkResult();
-}
-
-void AuthContext::checkResult() {
- qDebug() << "AuthContext::checkResult called";
- if(m_requestsDone != 2) {
- qDebug() << "Number of ready results:" << m_requestsDone;
- return;
- }
- if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
- doEntitlements();
- }
- else {
- finishActivity();
- if(stsFailed) {
- if(stsErrors.contains(2148916233)) {
- changeState(
- STATE_FAILED_HARD,
- 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>")
- );
- }
- else if (stsErrors.contains(2148916235)){
- // NOTE: this is the Grulovia error
- changeState(
- STATE_FAILED_HARD,
- tr("XBox Live is not available in your country. You've been blocked.")
- );
- }
- else if (stsErrors.contains(2148916238)){
- changeState(
- STATE_FAILED_HARD,
- 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>")
- );
- }
- else {
- QStringList errorList;
- for(auto & error: stsErrors) {
- errorList.append(QString::number(error));
- }
- changeState(
- STATE_FAILED_HARD,
- tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorList.join("\n"))
- );
- }
- }
- else {
- changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed"));
- }
- }
-}
-
-void AuthContext::doEntitlements() {
- auto uuid = QUuid::createUuid();
- entitlementsRequestId = uuid.toString().remove('{').remove('}');
- auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + 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, &AuthContext::onEntitlementsDone);
- requestor->get(request);
- qDebug() << "Getting Xbox profile...";
-}
-
-
-void AuthContext::onEntitlementsDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << data;
-#endif
- // TODO: check presence of same entitlementsRequestId?
- // TODO: validate JWTs?
- Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
- doMinecraftProfile();
-}
-
-void AuthContext::doMinecraftProfile() {
- setStage(AuthStage::MinecraftProfile);
- changeState(STATE_WORKING, tr("Starting minecraft profile acquisition"));
-
- auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
- 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, &AuthContext::onMinecraftProfileDone);
- requestor->get(request);
-}
-
-void AuthContext::onMinecraftProfileDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#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();
- succeed();
- return;
- }
- if (error != QNetworkReply::NoError) {
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed."));
- return;
- }
- if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
- m_data->minecraftProfile = MinecraftProfile();
- finishActivity();
- changeState(STATE_FAILED_HARD, 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;
- doMigrationEligibilityCheck();
- }
- else {
- doGetSkin();
- }
-}
-
-void AuthContext::doMigrationEligibilityCheck() {
- setStage(AuthStage::MigrationEligibility);
- changeState(STATE_WORKING, tr("Starting check for migration eligibility"));
-
- 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, &AuthContext::onMigrationEligibilityCheckDone);
- requestor->get(request);
-}
-
-void AuthContext::onMigrationEligibilityCheckDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- if (error == QNetworkReply::NoError) {
- Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA);
- }
- doGetSkin();
-}
-
-void AuthContext::doGetSkin() {
- setStage(AuthStage::Skin);
- changeState(STATE_WORKING, tr("Fetching player skin"));
-
- auto url = QUrl(m_data->minecraftProfile.skin.url);
- QNetworkRequest request = QNetworkRequest(url);
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onSkinDone);
- requestor->get(request);
-}
-
-void AuthContext::onSkinDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair>
-) {
- if (error == QNetworkReply::NoError) {
- m_data->minecraftProfile.skin.data = data;
- }
- succeed();
-
-}
-
-void AuthContext::succeed() {
- m_data->validity_ = Katabasis::Validity::Certain;
- finishActivity();
- changeState(STATE_SUCCEEDED, tr("Finished all authentication steps"));
-}
-
-void AuthContext::setStage(AuthContext::AuthStage stage) {
- m_stage = stage;
- emit progress((int)m_stage, (int)AuthStage::Complete);
-}
-
-
-QString AuthContext::getStateMessage() const {
- switch (m_accountState)
- {
- case STATE_WORKING:
- switch(m_stage) {
- case AuthStage::Initial: {
- QString loginMessage = tr("Logging in as %1 user");
- if(m_data->type == AccountType::MSA) {
- return loginMessage.arg("Microsoft");
- }
- else {
- return loginMessage.arg("Mojang");
- }
- }
- case AuthStage::UserAuth:
- return tr("Logging in as XBox user");
- case AuthStage::XboxAuth:
- return tr("Logging in with XBox and Mojang services");
- case AuthStage::MinecraftProfile:
- return tr("Getting Minecraft profile");
- case AuthStage::MigrationEligibility:
- return tr("Checking for migration eligibility");
- case AuthStage::Skin:
- return tr("Getting Minecraft skin");
- case AuthStage::Complete:
- return tr("Finished");
- default:
- break;
- }
- default:
- return AccountTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h
deleted file mode 100644
index 5e4e9edc..00000000
--- a/launcher/minecraft/auth/flows/AuthContext.h
+++ /dev/null
@@ -1,110 +0,0 @@
-#pragma once
-
-#include <QObject>
-#include <QList>
-#include <QVector>
-#include <QSet>
-#include <QNetworkReply>
-#include <QImage>
-
-#include <katabasis/DeviceFlow.h>
-#include "Yggdrasil.h"
-#include "../AccountData.h"
-#include "../AccountTask.h"
-
-class AuthContext : public AccountTask
-{
- Q_OBJECT
-
-public:
- explicit AuthContext(AccountData * data, QObject *parent = 0);
-
- bool isBusy() {
- return m_activity != Katabasis::Activity::Idle;
- };
- Katabasis::Validity validity() {
- return m_data->validity_;
- };
-
- //bool signOut();
-
- QString getStateMessage() const override;
-
-signals:
- void activityChanged(Katabasis::Activity activity);
-
-private slots:
-// OAuth-specific callbacks
- void onOAuthActivityChanged(Katabasis::Activity activity);
-
-// Yggdrasil specific callbacks
- void onMojangSucceeded();
- void onMojangFailed();
-
-protected:
- void initMSA();
- void initMojang();
-
- void doUserAuth();
- Q_SLOT void onUserAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void processSTSError(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void doSTSAuthMinecraft();
- Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
- void doMinecraftAuth();
- Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void doSTSAuthGeneric();
- Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
- void doXBoxProfile();
- Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void doEntitlements();
- Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void doMinecraftProfile();
- Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void doMigrationEligibilityCheck();
- Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void doGetSkin();
- Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
-
- void succeed();
-
- void failResult(bool & flag);
- void succeedResult(bool & flag);
- void checkResult();
-
-protected:
- void beginActivity(Katabasis::Activity activity);
- void finishActivity();
- void clearTokens();
-
-protected:
- Katabasis::DeviceFlow *m_oauth2 = nullptr;
- Yggdrasil *m_yggdrasil = nullptr;
-
- int m_requestsDone = 0;
- bool m_xboxProfileSucceeded = false;
- bool m_mcAuthSucceeded = false;
- QString entitlementsRequestId;
-
- QSet<int64_t> stsErrors;
- bool stsFailed = false;
-
- Katabasis::Activity m_activity = Katabasis::Activity::Idle;
- enum class AuthStage {
- Initial,
- UserAuth,
- XboxAuth,
- MinecraftProfile,
- MigrationEligibility,
- Skin,
- Complete
- } m_stage = AuthStage::Initial;
-
- void setStage(AuthStage stage);
-};
diff --git a/launcher/minecraft/auth/flows/AuthFlow.cpp b/launcher/minecraft/auth/flows/AuthFlow.cpp
new file mode 100644
index 00000000..4f78e8c3
--- /dev/null
+++ b/launcher/minecraft/auth/flows/AuthFlow.cpp
@@ -0,0 +1,71 @@
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QDebug>
+
+#include "AuthFlow.h"
+#include "katabasis/Globals.h"
+
+#include <Application.h>
+
+AuthFlow::AuthFlow(AccountData * data, QObject *parent) :
+ AccountTask(data, parent)
+{
+}
+
+void AuthFlow::succeed() {
+ m_data->validity_ = Katabasis::Validity::Certain;
+ changeState(
+ AccountTaskState::STATE_SUCCEEDED,
+ tr("Finished all authentication steps")
+ );
+}
+
+void AuthFlow::executeTask() {
+ if(m_currentStep) {
+ return;
+ }
+ changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
+ nextStep();
+}
+
+void AuthFlow::nextStep() {
+ if(m_steps.size() == 0) {
+ // we got to the end without an incident... assume this is all.
+ m_currentStep.reset();
+ succeed();
+ return;
+ }
+ m_currentStep = m_steps.front();
+ qDebug() << "AuthFlow:" << m_currentStep->describe();
+ m_steps.pop_front();
+ connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
+ connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode);
+ connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode);
+
+ m_currentStep->perform();
+}
+
+
+QString AuthFlow::getStateMessage() const {
+ switch (m_taskState)
+ {
+ case AccountTaskState::STATE_WORKING: {
+ if(m_currentStep) {
+ return m_currentStep->describe();
+ }
+ else {
+ return tr("Working...");
+ }
+ }
+ default: {
+ return AccountTask::getStateMessage();
+ }
+ }
+}
+
+void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) {
+ if(changeState(resultingState, message)) {
+ nextStep();
+ }
+}
diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h
new file mode 100644
index 00000000..e067cc99
--- /dev/null
+++ b/launcher/minecraft/auth/flows/AuthFlow.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <QObject>
+#include <QList>
+#include <QVector>
+#include <QSet>
+#include <QNetworkReply>
+#include <QImage>
+
+#include <katabasis/DeviceFlow.h>
+
+#include "minecraft/auth/Yggdrasil.h"
+#include "minecraft/auth/AccountData.h"
+#include "minecraft/auth/AccountTask.h"
+#include "minecraft/auth/AuthStep.h"
+
+class AuthFlow : public AccountTask
+{
+ Q_OBJECT
+
+public:
+ explicit AuthFlow(AccountData * data, QObject *parent = 0);
+
+ Katabasis::Validity validity() {
+ return m_data->validity_;
+ };
+
+ QString getStateMessage() const override;
+
+ void executeTask() override;
+
+signals:
+ void activityChanged(Katabasis::Activity activity);
+
+private slots:
+ void stepFinished(AccountTaskState resultingState, QString message);
+
+protected:
+ void succeed();
+ void nextStep();
+
+protected:
+ QList<AuthStep::Ptr> m_steps;
+ AuthStep::Ptr m_currentStep;
+};
diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp
new file mode 100644
index 00000000..416b8f2c
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSA.cpp
@@ -0,0 +1,37 @@
+#include "MSA.h"
+
+#include "minecraft/auth/steps/MSAStep.h"
+#include "minecraft/auth/steps/XboxUserStep.h"
+#include "minecraft/auth/steps/XboxAuthorizationStep.h"
+#include "minecraft/auth/steps/LauncherLoginStep.h"
+#include "minecraft/auth/steps/XboxProfileStep.h"
+#include "minecraft/auth/steps/EntitlementsStep.h"
+#include "minecraft/auth/steps/MinecraftProfileStep.h"
+#include "minecraft/auth/steps/GetSkinStep.h"
+
+MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) {
+ m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh));
+ m_steps.append(new XboxUserStep(m_data));
+ m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
+ m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(new LauncherLoginStep(m_data));
+ m_steps.append(new XboxProfileStep(m_data));
+ m_steps.append(new EntitlementsStep(m_data));
+ m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new GetSkinStep(m_data));
+}
+
+MSAInteractive::MSAInteractive(
+ AccountData* data,
+ QObject* parent
+) : AuthFlow(data, parent) {
+ m_steps.append(new MSAStep(m_data, MSAStep::Action::Login));
+ m_steps.append(new XboxUserStep(m_data));
+ m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
+ m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(new LauncherLoginStep(m_data));
+ m_steps.append(new XboxProfileStep(m_data));
+ m_steps.append(new EntitlementsStep(m_data));
+ m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new GetSkinStep(m_data));
+}
diff --git a/launcher/minecraft/auth/flows/MSA.h b/launcher/minecraft/auth/flows/MSA.h
new file mode 100644
index 00000000..14a4ff43
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSA.h
@@ -0,0 +1,22 @@
+#pragma once
+#include "AuthFlow.h"
+
+class MSAInteractive : public AuthFlow
+{
+ Q_OBJECT
+public:
+ explicit MSAInteractive(
+ AccountData *data,
+ QObject *parent = 0
+ );
+};
+
+class MSASilent : public AuthFlow
+{
+ Q_OBJECT
+public:
+ explicit MSASilent(
+ AccountData * data,
+ QObject *parent = 0
+ );
+};
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp
deleted file mode 100644
index 525aaf88..00000000
--- a/launcher/minecraft/auth/flows/MSAInteractive.cpp
+++ /dev/null
@@ -1,22 +0,0 @@
-#include "MSAInteractive.h"
-
-MSAInteractive::MSAInteractive(
- AccountData* data,
- QObject* parent
-) : AuthContext(data, parent) {}
-
-void MSAInteractive::executeTask() {
- m_requestsDone = 0;
- m_xboxProfileSucceeded = false;
- m_mcAuthSucceeded = false;
-
- initMSA();
-
- QVariantMap extraOpts;
- extraOpts["prompt"] = "select_account";
- m_oauth2->setExtraRequestParams(extraOpts);
-
- beginActivity(Katabasis::Activity::LoggingIn);
- *m_data = AccountData();
- m_oauth2->login();
-}
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h
deleted file mode 100644
index 6654e0d6..00000000
--- a/launcher/minecraft/auth/flows/MSAInteractive.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-#include "AuthContext.h"
-
-class MSAInteractive : public AuthContext
-{
- Q_OBJECT
-public:
- explicit MSAInteractive(
- AccountData *data,
- QObject *parent = 0
- );
- void executeTask() override;
-};
diff --git a/launcher/minecraft/auth/flows/MSASilent.cpp b/launcher/minecraft/auth/flows/MSASilent.cpp
deleted file mode 100644
index 8ce43c1f..00000000
--- a/launcher/minecraft/auth/flows/MSASilent.cpp
+++ /dev/null
@@ -1,16 +0,0 @@
-#include "MSASilent.h"
-
-MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
-
-void MSASilent::executeTask() {
- m_requestsDone = 0;
- m_xboxProfileSucceeded = false;
- m_mcAuthSucceeded = false;
-
- initMSA();
-
- beginActivity(Katabasis::Activity::Refreshing);
- if(!m_oauth2->refresh()) {
- finishActivity();
- }
-}
diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h
deleted file mode 100644
index a442b49e..00000000
--- a/launcher/minecraft/auth/flows/MSASilent.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-#include "AuthContext.h"
-
-class MSASilent : public AuthContext
-{
- Q_OBJECT
-public:
- explicit MSASilent(
- AccountData * data,
- QObject *parent = 0
- );
- void executeTask() override;
-};
diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp
new file mode 100644
index 00000000..4661dbe2
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Mojang.cpp
@@ -0,0 +1,27 @@
+#include "Mojang.h"
+
+#include "minecraft/auth/steps/YggdrasilStep.h"
+#include "minecraft/auth/steps/MinecraftProfileStep.h"
+#include "minecraft/auth/steps/MigrationEligibilityStep.h"
+#include "minecraft/auth/steps/GetSkinStep.h"
+
+MojangRefresh::MojangRefresh(
+ AccountData *data,
+ QObject *parent
+) : AuthFlow(data, parent) {
+ m_steps.append(new YggdrasilStep(m_data, QString()));
+ m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new MigrationEligibilityStep(m_data));
+ m_steps.append(new GetSkinStep(m_data));
+}
+
+MojangLogin::MojangLogin(
+ AccountData *data,
+ QString password,
+ QObject *parent
+): AuthFlow(data, parent), m_password(password) {
+ m_steps.append(new YggdrasilStep(m_data, m_password));
+ 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/flows/Mojang.h b/launcher/minecraft/auth/flows/Mojang.h
new file mode 100644
index 00000000..c09c81a8
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Mojang.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "AuthFlow.h"
+
+class MojangRefresh : public AuthFlow
+{
+ Q_OBJECT
+public:
+ explicit MojangRefresh(
+ AccountData *data,
+ QObject *parent = 0
+ );
+};
+
+class MojangLogin : public AuthFlow
+{
+ Q_OBJECT
+public:
+ explicit MojangLogin(
+ AccountData *data,
+ QString password,
+ QObject *parent = 0
+ );
+
+private:
+ QString m_password;
+};
diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp
deleted file mode 100644
index 6c217cd1..00000000
--- a/launcher/minecraft/auth/flows/MojangLogin.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-#include "MojangLogin.h"
-
-MojangLogin::MojangLogin(
- AccountData *data,
- QString password,
- QObject *parent
-): AuthContext(data, parent), m_password(password) {}
-
-void MojangLogin::executeTask() {
- m_requestsDone = 0;
- m_xboxProfileSucceeded = false;
- m_mcAuthSucceeded = false;
-
- initMojang();
-
- beginActivity(Katabasis::Activity::LoggingIn);
- m_yggdrasil->login(m_password);
-}
diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h
deleted file mode 100644
index 5f33752f..00000000
--- a/launcher/minecraft/auth/flows/MojangLogin.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-#include "AuthContext.h"
-
-class MojangLogin : public AuthContext
-{
- Q_OBJECT
-public:
- explicit MojangLogin(
- AccountData *data,
- QString password,
- QObject *parent = 0
- );
- void executeTask() override;
-
-private:
- QString m_password;
-};
diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp
deleted file mode 100644
index 008c0453..00000000
--- a/launcher/minecraft/auth/flows/MojangRefresh.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#include "MojangRefresh.h"
-
-MojangRefresh::MojangRefresh(
- AccountData *data,
- QObject *parent
-) : AuthContext(data, parent) {}
-
-void MojangRefresh::executeTask() {
- m_requestsDone = 0;
- m_xboxProfileSucceeded = false;
- m_mcAuthSucceeded = false;
-
- initMojang();
-
- beginActivity(Katabasis::Activity::Refreshing);
- m_yggdrasil->refresh();
-}
diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h
deleted file mode 100644
index 06e4e4ce..00000000
--- a/launcher/minecraft/auth/flows/MojangRefresh.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma once
-#include "AuthContext.h"
-
-class MojangRefresh : public AuthContext
-{
- Q_OBJECT
-public:
- explicit MojangRefresh(AccountData *data, QObject *parent = 0);
- void executeTask() override;
-};
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..9fef99b0
--- /dev/null
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -0,0 +1,83 @@
+#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) {
+ 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..ac6ad798
--- /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(AccountTaskState::STATE_OFFLINE, 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;
+};