aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2021-07-26 21:44:11 +0200
committerPetr Mrázek <peterix@gmail.com>2021-08-15 23:18:50 +0200
commit3a53349e332599221bc325f7fac9dc7927194bc2 (patch)
tree2ee40fa6044c241b3b7db27fe0b83931b453c2b2 /launcher
parentfca2e9e44cb44004eec7f47c03b186bd5e44dc32 (diff)
downloadPrismLauncher-3a53349e332599221bc325f7fac9dc7927194bc2.tar.gz
PrismLauncher-3a53349e332599221bc325f7fac9dc7927194bc2.tar.bz2
PrismLauncher-3a53349e332599221bc325f7fac9dc7927194bc2.zip
GH-3392 dirty initial MSA support that shares logic with Mojang flows
Both act as the first step of AuthContext.
Diffstat (limited to 'launcher')
-rw-r--r--launcher/BaseInstance.h2
-rw-r--r--launcher/CMakeLists.txt39
-rw-r--r--launcher/Env.cpp1
-rw-r--r--launcher/LaunchController.cpp171
-rw-r--r--launcher/MainWindow.cpp158
-rw-r--r--launcher/MainWindow.h5
-rw-r--r--launcher/MultiMC.cpp4
-rw-r--r--launcher/MultiMC.h6
-rw-r--r--launcher/SkinUtils.cpp4
-rw-r--r--launcher/dialogs/LoginDialog.cpp9
-rw-r--r--launcher/dialogs/LoginDialog.h6
-rw-r--r--launcher/dialogs/LoginDialog.ui12
-rw-r--r--launcher/dialogs/MSALoginDialog.cpp96
-rw-r--r--launcher/dialogs/MSALoginDialog.h55
-rw-r--r--launcher/dialogs/MSALoginDialog.ui60
-rw-r--r--launcher/dialogs/ProfileSelectDialog.cpp32
-rw-r--r--launcher/dialogs/ProfileSelectDialog.h8
-rw-r--r--launcher/dialogs/SkinUploadDialog.cpp2
-rw-r--r--launcher/dialogs/SkinUploadDialog.h6
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp16
-rw-r--r--launcher/minecraft/auth-msa/BuildConfig.cpp.in9
-rw-r--r--launcher/minecraft/auth-msa/BuildConfig.h11
-rw-r--r--launcher/minecraft/auth-msa/CMakeLists.txt28
-rw-r--r--launcher/minecraft/auth-msa/main.cpp100
-rw-r--r--launcher/minecraft/auth-msa/mainwindow.cpp97
-rw-r--r--launcher/minecraft/auth-msa/mainwindow.h34
-rw-r--r--launcher/minecraft/auth-msa/mainwindow.ui72
-rw-r--r--launcher/minecraft/auth/AccountData.cpp387
-rw-r--r--launcher/minecraft/auth/AccountData.h73
-rw-r--r--launcher/minecraft/auth/AccountList.cpp (renamed from launcher/minecraft/auth/MojangAccountList.cpp)321
-rw-r--r--launcher/minecraft/auth/AccountList.h118
-rw-r--r--launcher/minecraft/auth/AccountTask.cpp69
-rw-r--r--launcher/minecraft/auth/AccountTask.h (renamed from launcher/minecraft/auth/YggdrasilTask.h)78
-rw-r--r--launcher/minecraft/auth/AuthSession.cpp2
-rw-r--r--launcher/minecraft/auth/AuthSession.h13
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp303
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.h (renamed from launcher/minecraft/auth/MojangAccount.h)120
-rw-r--r--launcher/minecraft/auth/MojangAccount.cpp315
-rw-r--r--launcher/minecraft/auth/MojangAccountList.h199
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp (renamed from launcher/minecraft/auth-msa/context.cpp)508
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.h (renamed from launcher/minecraft/auth-msa/context.h)108
-rw-r--r--launcher/minecraft/auth/flows/AuthenticateTask.cpp202
-rw-r--r--launcher/minecraft/auth/flows/AuthenticateTask.h46
-rw-r--r--launcher/minecraft/auth/flows/MSAHelper.txt51
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.cpp20
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.h10
-rw-r--r--launcher/minecraft/auth/flows/MSASilent.cpp16
-rw-r--r--launcher/minecraft/auth/flows/MSASilent.h10
-rw-r--r--launcher/minecraft/auth/flows/MojangLogin.cpp14
-rw-r--r--launcher/minecraft/auth/flows/MojangLogin.h13
-rw-r--r--launcher/minecraft/auth/flows/MojangRefresh.cpp14
-rw-r--r--launcher/minecraft/auth/flows/MojangRefresh.h10
-rw-r--r--launcher/minecraft/auth/flows/RefreshTask.cpp144
-rw-r--r--launcher/minecraft/auth/flows/RefreshTask.h44
-rw-r--r--launcher/minecraft/auth/flows/ValidateTask.cpp61
-rw-r--r--launcher/minecraft/auth/flows/ValidateTask.h47
-rw-r--r--launcher/minecraft/auth/flows/Yggdrasil.cpp (renamed from launcher/minecraft/auth/YggdrasilTask.cpp)224
-rw-r--r--launcher/minecraft/auth/flows/Yggdrasil.h82
-rw-r--r--launcher/minecraft/launch/ClaimAccount.h4
-rw-r--r--launcher/pages/global/AccountListPage.cpp80
-rw-r--r--launcher/pages/global/AccountListPage.h8
-rw-r--r--launcher/pages/global/AccountListPage.ui30
-rw-r--r--launcher/pages/instance/VersionPage.cpp2
63 files changed, 2323 insertions, 2466 deletions
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index 833646c0..8c08dc05 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -26,7 +26,7 @@
#include "settings/INIFile.h"
#include "BaseVersionList.h"
-#include "minecraft/auth/MojangAccount.h"
+#include "minecraft/auth/MinecraftAccount.h"
#include "MessageLevel.h"
#include "pathmatcher/IPathMatcher.h"
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 37f5d3a1..3c140ede 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -203,20 +203,31 @@ set(STATUS_SOURCES
# Support for Minecraft instances and launch
set(MINECRAFT_SOURCES
# Minecraft support
+ minecraft/auth/AccountData.h
+ minecraft/auth/AccountData.cpp
+ minecraft/auth/AccountTask.h
+ minecraft/auth/AccountTask.cpp
minecraft/auth/AuthSession.h
minecraft/auth/AuthSession.cpp
- minecraft/auth/MojangAccountList.h
- minecraft/auth/MojangAccountList.cpp
- minecraft/auth/MojangAccount.h
- minecraft/auth/MojangAccount.cpp
- minecraft/auth/YggdrasilTask.h
- minecraft/auth/YggdrasilTask.cpp
- minecraft/auth/flows/AuthenticateTask.h
- minecraft/auth/flows/AuthenticateTask.cpp
- minecraft/auth/flows/RefreshTask.cpp
- minecraft/auth/flows/RefreshTask.cpp
- minecraft/auth/flows/ValidateTask.h
- minecraft/auth/flows/ValidateTask.cpp
+ minecraft/auth/AccountList.h
+ minecraft/auth/AccountList.cpp
+ minecraft/auth/MinecraftAccount.h
+ minecraft/auth/MinecraftAccount.cpp
+ minecraft/auth/flows/AuthContext.h
+ minecraft/auth/flows/AuthContext.cpp
+
+ minecraft/auth/flows/MSAInteractive.h
+ minecraft/auth/flows/MSAInteractive.cpp
+ minecraft/auth/flows/MSASilent.h
+ minecraft/auth/flows/MSASilent.cpp
+
+ minecraft/auth/flows/MojangLogin.h
+ minecraft/auth/flows/MojangLogin.cpp
+ minecraft/auth/flows/MojangRefresh.h
+ minecraft/auth/flows/MojangRefresh.cpp
+
+ minecraft/auth/flows/Yggdrasil.h
+ minecraft/auth/flows/Yggdrasil.cpp
minecraft/gameoptions/GameOptions.h
minecraft/gameoptions/GameOptions.cpp
@@ -732,6 +743,8 @@ SET(MULTIMC_SOURCES
dialogs/IconPickerDialog.h
dialogs/LoginDialog.cpp
dialogs/LoginDialog.h
+ dialogs/MSALoginDialog.cpp
+ dialogs/MSALoginDialog.h
dialogs/NewComponentDialog.cpp
dialogs/NewComponentDialog.h
dialogs/NewInstanceDialog.cpp
@@ -850,6 +863,7 @@ SET(MULTIMC_UIS
dialogs/EditAccountDialog.ui
dialogs/ExportInstanceDialog.ui
dialogs/LoginDialog.ui
+ dialogs/MSALoginDialog.ui
dialogs/UpdateDialog.ui
dialogs/NotificationDialog.ui
dialogs/SkinUploadDialog.ui
@@ -892,6 +906,7 @@ target_link_libraries(MultiMC_logic
optional-bare
tomlc99
BuildConfig
+ Katabasis
)
target_link_libraries(MultiMC_logic
Qt5::Core
diff --git a/launcher/Env.cpp b/launcher/Env.cpp
index 71b49d95..abf9f58c 100644
--- a/launcher/Env.cpp
+++ b/launcher/Env.cpp
@@ -101,7 +101,6 @@ void Env::initHttpMetaCache()
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
- m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath());
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index ee764082..11780625 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -1,6 +1,6 @@
#include "LaunchController.h"
#include "MainWindow.h"
-#include <minecraft/auth/MojangAccountList.h>
+#include <minecraft/auth/AccountList.h>
#include "MultiMC.h"
#include "dialogs/CustomMessageBox.h"
#include "dialogs/ProfileSelectDialog.h"
@@ -12,7 +12,7 @@
#include <QLineEdit>
#include <QInputDialog>
#include <tasks/Task.h>
-#include <minecraft/auth/YggdrasilTask.h>
+#include <minecraft/auth/AccountTask.h>
#include <launch/steps/TextPrint.h>
#include <QStringList>
#include <QHostInfo>
@@ -35,22 +35,23 @@ void LaunchController::executeTask()
}
// FIXME: minecraft specific
-void LaunchController::login()
-{
+void LaunchController::login() {
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
// Find an account to use.
- std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
- MojangAccountPtr account = accounts->activeAccount();
+ std::shared_ptr<AccountList> accounts = MMC->accounts();
if (accounts->count() <= 0)
{
// Tell the user they need to log in at least one account in order to play.
auto reply = CustomMessageBox::selectable(
- m_parentWidget, tr("No Accounts"),
+ m_parentWidget,
+ tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Mojang or Minecraft "
"account logged in to MultiMC."
"Would you like to open the account manager to add an account now?"),
- QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec();
+ QMessageBox::Information,
+ QMessageBox::Yes | QMessageBox::No
+ )->exec();
if (reply == QMessageBox::Yes)
{
@@ -58,11 +59,16 @@ void LaunchController::login()
MMC->ShowGlobalSettings(m_parentWidget, "accounts");
}
}
- else if (account.get() == nullptr)
+
+ MinecraftAccountPtr account = accounts->activeAccount();
+ if (account.get() == nullptr)
{
// If no default account is set, ask the user which one to use.
- ProfileSelectDialog selectDialog(tr("Which profile would you like to use?"),
- ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget);
+ ProfileSelectDialog selectDialog(
+ tr("Which account would you like to use?"),
+ ProfileSelectDialog::GlobalDefaultCheckbox,
+ m_parentWidget
+ );
selectDialog.exec();
@@ -70,8 +76,9 @@ void LaunchController::login()
account = selectDialog.selectedAccount();
// If the user said to use the account as default, do that.
- if (selectDialog.useAsGlobalDefault() && account.get() != nullptr)
- accounts->setActiveAccount(account->username());
+ if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) {
+ accounts->setActiveAccount(account->profileId());
+ }
}
// if no account is selected, we bail
@@ -93,7 +100,13 @@ void LaunchController::login()
{
m_session = std::make_shared<AuthSession>();
m_session->wants_online = m_online;
- auto task = account->login(m_session, password);
+ std::shared_ptr<AccountTask> task;
+ if(!password.isNull()) {
+ task = account->login(m_session, password);
+ }
+ else {
+ task = account->refresh(m_session);
+ }
if (task)
{
// We'll need to validate the access token to make sure the account
@@ -107,9 +120,9 @@ void LaunchController::login()
if (!task->wasSuccessful())
{
auto failReasonNew = task->failReason();
- if(failReasonNew == "Invalid token.")
+ if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature")
{
- account->invalidateClientToken();
+ // account->invalidateClientToken();
failReason = needLoginAgain;
}
else failReason = failReasonNew;
@@ -117,72 +130,82 @@ void LaunchController::login()
}
switch (m_session->status)
{
- case AuthSession::Undetermined:
- {
- qCritical() << "Received undetermined session status during login. Bye.";
- tryagain = false;
- emitFailed(tr("Received undetermined session status during login."));
- break;
- }
- case AuthSession::RequiresPassword:
- {
- EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField);
- auto username = m_session->username;
- auto chopN = [](QString toChop, int N) -> QString
- {
- if(toChop.size() > N)
+ case AuthSession::Undetermined: {
+ qCritical() << "Received undetermined session status during login. Bye.";
+ tryagain = false;
+ emitFailed(tr("Received undetermined session status during login."));
+ return;
+ }
+ case AuthSession::RequiresPassword: {
+ // FIXME: this needs to understand MSA
+ EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField);
+ auto username = m_session->username;
+ auto chopN = [](QString toChop, int N) -> QString
{
- auto left = toChop.left(N);
- left += QString("\u25CF").repeated(toChop.size() - N);
- return left;
- }
- return toChop;
- };
+ if(toChop.size() > N)
+ {
+ auto left = toChop.left(N);
+ left += QString("\u25CF").repeated(toChop.size() - N);
+ return left;
+ }
+ return toChop;
+ };
- if(username.contains('@'))
- {
- auto parts = username.split('@');
- auto mailbox = chopN(parts[0],3);
- QString domain = chopN(parts[1], 3);
- username = mailbox + '@' + domain;
- }
- passDialog.setUsername(username);
- if (passDialog.exec() == QDialog::Accepted)
- {
- password = passDialog.password();
+ if(username.contains('@'))
+ {
+ auto parts = username.split('@');
+ auto mailbox = chopN(parts[0],3);
+ QString domain = chopN(parts[1], 3);
+ username = mailbox + '@' + domain;
+ }
+ passDialog.setUsername(username);
+ if (passDialog.exec() == QDialog::Accepted)
+ {
+ password = passDialog.password();
+ }
+ else
+ {
+ tryagain = false;
+ emitFailed(tr("Received undetermined session status during login."));
+ }
+ break;
}
- else
- {
+ case AuthSession::RequiresOAuth: {
+ // FIXME: add UI for expired / broken MS accounts
tryagain = false;
+ emitFailed(tr("Microsoft account has expired and needs to be logged into again."));
+ return;
}
- break;
- }
- case AuthSession::PlayableOffline:
- {
- // we ask the user for a player name
- bool ok = false;
- QString usedname = m_session->player_name;
- QString name = QInputDialog::getText(m_parentWidget, tr("Player name"),
- tr("Choose your offline mode player name."),
- QLineEdit::Normal, m_session->player_name, &ok);
- if (!ok)
- {
- tryagain = false;
- break;
+ case AuthSession::PlayableOffline: {
+ // we ask the user for a player name
+ bool ok = false;
+ QString usedname = m_session->player_name;
+ QString name = QInputDialog::getText(
+ m_parentWidget,
+ tr("Player name"),
+ tr("Choose your offline mode player name."),
+ QLineEdit::Normal,
+ m_session->player_name,
+ &ok
+ );
+ if (!ok)
+ {
+ tryagain = false;
+ break;
+ }
+ if (name.length())
+ {
+ usedname = name;
+ }
+ m_session->MakeOffline(usedname);
+ // offline flavored game from here :3
}
- if (name.length())
+ case AuthSession::PlayableOnline:
{
- usedname = name;
+ launchInstance();
+ tryagain = false;
+ return;
}
- m_session->MakeOffline(usedname);
- // offline flavored game from here :3
- }
- case AuthSession::PlayableOnline:
- {
- launchInstance();
- tryagain = false;
- return;
- }
}
}
emitFailed(tr("Failed to launch."));
diff --git a/launcher/MainWindow.cpp b/launcher/MainWindow.cpp
index 9225193e..182b22e9 100644
--- a/launcher/MainWindow.cpp
+++ b/launcher/MainWindow.cpp
@@ -54,7 +54,7 @@
#include <java/JavaUtils.h>
#include <java/JavaInstallList.h>
#include <launch/LaunchTask.h>
-#include <minecraft/auth/MojangAccountList.h>
+#include <minecraft/auth/AccountList.h>
#include <SkinUtils.h>
#include <BuildConfig.h>
#include <net/NetJob.h>
@@ -90,6 +90,20 @@
#include "KonamiCode.h"
#include <InstanceCopyTask.h>
+namespace {
+QString profileInUseFilter(const QString & profile, bool used)
+{
+ if(used)
+ {
+ return QObject::tr("%1 (in use)").arg(profile);
+ }
+ else
+ {
+ return profile;
+ }
+}
+}
+
// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code
template <typename T>
class Translated
@@ -753,49 +767,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
// Update the menu when the active account changes.
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
// Template hell sucks...
- connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this]
- {
- activeAccountChanged();
- });
- connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this]
- {
- repopulateAccountsMenu();
- });
+ connect(
+ MMC->accounts().get(),
+ &AccountList::activeAccountChanged,
+ [this] {
+ activeAccountChanged();
+ }
+ );
+ connect(
+ MMC->accounts().get(),
+ &AccountList::listChanged,
+ [this]
+ {
+ repopulateAccountsMenu();
+ }
+ );
// Show initial account
activeAccountChanged();
- auto accounts = MMC->accounts();
-
- QList<Net::Download::Ptr> skin_dls;
- for (int i = 0; i < accounts->count(); i++)
- {
- auto account = accounts->at(i);
- if (!account)
- {
- qWarning() << "Null account at index" << i;
- continue;
- }
- for (auto profile : account->profiles())
- {
- auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png");
- auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta);
- skin_dls.append(action);
- meta->setStale(true);
- }
- }
- if (!skin_dls.isEmpty())
- {
- auto job = new NetJob("Startup player skins download");
- connect(job, &NetJob::succeeded, this, &MainWindow::skinJobFinished);
- connect(job, &NetJob::failed, this, &MainWindow::skinJobFinished);
- for (auto action : skin_dls)
- {
- job->addNetAction(action);
- }
- skin_download_job.reset(job);
- job->start();
- }
+ // TODO: refresh accounts here?
+ // auto accounts = MMC->accounts();
// load the news
{
@@ -844,7 +836,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
void MainWindow::retranslateUi()
{
- accountMenuButton->setText(tr("Profiles"));
+ std::shared_ptr<AccountList> accounts = MMC->accounts();
+ MinecraftAccountPtr active_account = accounts->activeAccount();
+ if(active_account) {
+ auto profileLabel = profileInUseFilter(active_account->profileName(), active_account->isInUse());
+ accountMenuButton->setText(profileLabel);
+ }
+ else {
+ accountMenuButton->setText(tr("Profiles"));
+ }
if (m_selectedInstance) {
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
@@ -872,12 +872,6 @@ void MainWindow::konamiTriggered()
qDebug() << "Super Secret Mode ACTIVATED!";
}
-void MainWindow::skinJobFinished()
-{
- activeAccountChanged();
- skin_download_job.reset();
-}
-
void MainWindow::showInstanceContextMenu(const QPoint &pos)
{
QList<QAction *> actions;
@@ -1018,34 +1012,21 @@ void MainWindow::updateToolsMenu()
ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu);
}
-QString profileInUseFilter(const QString & profile, bool used)
-{
- if(used)
- {
- return profile + QObject::tr(" (in use)");
- }
- else
- {
- return profile;
- }
-}
-
void MainWindow::repopulateAccountsMenu()
{
accountMenu->clear();
- std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
- MojangAccountPtr active_account = accounts->activeAccount();
+ std::shared_ptr<AccountList> accounts = MMC->accounts();
+ MinecraftAccountPtr active_account = accounts->activeAccount();
- QString active_username = "";
+ QString active_profileId = "";
if (active_account != nullptr)
{
- active_username = active_account->username();
- const AccountProfile *profile = active_account->currentProfile();
+ active_profileId = active_account->profileId();
// this can be called before accountMenuButton exists
- if (profile != nullptr && accountMenuButton)
+ if (accountMenuButton)
{
- auto profileLabel = profileInUseFilter(profile->name, active_account->isInUse());
+ auto profileLabel = profileInUseFilter(active_account->profileName(), active_account->isInUse());
accountMenuButton->setText(profileLabel);
}
}
@@ -1061,22 +1042,19 @@ void MainWindow::repopulateAccountsMenu()
// TODO: Nicer way to iterate?
for (int i = 0; i < accounts->count(); i++)
{
- MojangAccountPtr account = accounts->at(i);
- for (auto profile : account->profiles())
+ MinecraftAccountPtr account = accounts->at(i);
+ auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
+ QAction *action = new QAction(profileLabel, this);
+ action->setData(account->profileId());
+ action->setCheckable(true);
+ if (active_profileId == account->profileId())
{
- auto profileLabel = profileInUseFilter(profile.name, account->isInUse());
- QAction *action = new QAction(profileLabel, this);
- action->setData(account->username());
- action->setCheckable(true);
- if (active_username == account->username())
- {
- action->setChecked(true);
- }
-
- action->setIcon(SkinUtils::getFaceFromCache(profile.id));
- accountMenu->addAction(action);
- connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
+ action->setChecked(true);
}
+
+ action->setIcon(account->getFace());
+ accountMenu->addAction(action);
+ connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
}
}
@@ -1086,8 +1064,7 @@ void MainWindow::repopulateAccountsMenu()
action->setCheckable(true);
action->setIcon(MMC->getThemedIcon("noaccount"));
action->setData("");
- if (active_username.isEmpty())
- {
+ if (active_profileId.isEmpty()) {
action->setChecked(true);
}
@@ -1134,18 +1111,15 @@ void MainWindow::activeAccountChanged()
{
repopulateAccountsMenu();
- MojangAccountPtr account = MMC->accounts()->activeAccount();
+ MinecraftAccountPtr account = MMC->accounts()->activeAccount();
- if (account != nullptr && account->username() != "")
+ // FIXME: this needs adjustment for MSA
+ if (account != nullptr && account->profileName() != "")
{
- const AccountProfile *profile = account->currentProfile();
- if (profile != nullptr)
- {
- auto profileLabel = profileInUseFilter(profile->name, account->isInUse());
- accountMenuButton->setIcon(SkinUtils::getFaceFromCache(profile->id));
- accountMenuButton->setText(profileLabel);
- return;
- }
+ auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
+ accountMenuButton->setText(profileLabel);
+ accountMenuButton->setIcon(account->getFace());
+ return;
}
// Set the icon to the "no account" icon.
diff --git a/launcher/MainWindow.h b/launcher/MainWindow.h
index c992ab94..67dec8cf 100644
--- a/launcher/MainWindow.h
+++ b/launcher/MainWindow.h
@@ -22,7 +22,7 @@
#include <QTimer>
#include "BaseInstance.h"
-#include "minecraft/auth/MojangAccount.h"
+#include "minecraft/auth/MinecraftAccount.h"
#include "net/NetJob.h"
#include "updater/GoUpdate.h"
@@ -149,8 +149,6 @@ private slots:
void updateToolsMenu();
- void skinJobFinished();
-
void instanceActivated(QModelIndex);
void instanceChanged(const QModelIndex &current, const QModelIndex &previous);
@@ -214,7 +212,6 @@ private:
QToolButton *accountMenuButton = nullptr;
KonamiCode * secretEventFilter = nullptr;
- unique_qobject_ptr<NetJob> skin_download_job;
unique_qobject_ptr<NewsChecker> m_newsChecker;
unique_qobject_ptr<NotificationChecker> m_notificationChecker;
diff --git a/launcher/MultiMC.cpp b/launcher/MultiMC.cpp
index 932c7a76..5961a45d 100644
--- a/launcher/MultiMC.cpp
+++ b/launcher/MultiMC.cpp
@@ -42,7 +42,7 @@
#include "dialogs/CustomMessageBox.h"
#include "InstanceList.h"
-#include <minecraft/auth/MojangAccountList.h>
+#include <minecraft/auth/AccountList.h>
#include "icons/IconList.h"
#include "net/HttpMetaCache.h"
#include "Env.h"
@@ -745,7 +745,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
// and accounts
{
- m_accounts.reset(new MojangAccountList(this));
+ m_accounts.reset(new AccountList(this));
qDebug() << "Loading accounts...";
m_accounts->setListFilePath("accounts.json", true);
m_accounts->loadList();
diff --git a/launcher/MultiMC.h b/launcher/MultiMC.h
index af2b41c1..59fd7345 100644
--- a/launcher/MultiMC.h
+++ b/launcher/MultiMC.h
@@ -24,7 +24,7 @@ class QFile;
class HttpMetaCache;
class SettingsObject;
class InstanceList;
-class MojangAccountList;
+class AccountList;
class IconList;
class QNetworkAccessManager;
class JavaInstallList;
@@ -111,7 +111,7 @@ public:
return m_mcedit.get();
}
- std::shared_ptr<MojangAccountList> accounts() const
+ std::shared_ptr<AccountList> accounts() const
{
return m_accounts;
}
@@ -188,7 +188,7 @@ private:
FolderInstanceProvider * m_instanceFolder = nullptr;
std::shared_ptr<IconList> m_icons;
std::shared_ptr<UpdateChecker> m_updateChecker;
- std::shared_ptr<MojangAccountList> m_accounts;
+ std::shared_ptr<AccountList> m_accounts;
std::shared_ptr<JavaInstallList> m_javalist;
std::shared_ptr<TranslationsModel> m_translations;
std::shared_ptr<GenericPageProvider> m_globalSettingsProvider;
diff --git a/launcher/SkinUtils.cpp b/launcher/SkinUtils.cpp
index ec969889..a196173e 100644
--- a/launcher/SkinUtils.cpp
+++ b/launcher/SkinUtils.cpp
@@ -30,9 +30,7 @@ namespace SkinUtils
*/
QPixmap getFaceFromCache(QString username, int height, int width)
{
- QFile fskin(ENV.metacache()
- ->resolveEntry("skins", username + ".png")
- ->getFullPath());
+ QFile fskin(ENV.metacache()->resolveEntry("skins", username + ".png")->getFullPath());
if (fskin.exists())
{
diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp
index 32f8a48f..1dee9920 100644
--- a/launcher/dialogs/LoginDialog.cpp
+++ b/launcher/dialogs/LoginDialog.cpp
@@ -16,7 +16,7 @@
#include "LoginDialog.h"
#include "ui_LoginDialog.h"
-#include "minecraft/auth/YggdrasilTask.h"
+#include "minecraft/auth/AccountTask.h"
#include <QtWidgets/QPushButton>
@@ -42,11 +42,10 @@ void LoginDialog::accept()
ui->progressBar->setVisible(true);
// Setup the login task and start it
- m_account = MojangAccount::createFromUsername(ui->userTextBox->text());
+ m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text());
m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
- connect(m_loginTask.get(), &Task::succeeded, this,
- &LoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
m_loginTask->start();
@@ -98,7 +97,7 @@ void LoginDialog::onTaskProgress(qint64 current, qint64 total)
}
// Public interface
-MojangAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
+MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
{
LoginDialog dlg(parent);
dlg.ui->label->setText(msg);
diff --git a/launcher/dialogs/LoginDialog.h b/launcher/dialogs/LoginDialog.h
index 16bdddfb..13463640 100644
--- a/launcher/dialogs/LoginDialog.h
+++ b/launcher/dialogs/LoginDialog.h
@@ -18,7 +18,7 @@
#include <QtWidgets/QDialog>
#include <QtCore/QEventLoop>
-#include "minecraft/auth/MojangAccount.h"
+#include "minecraft/auth/MinecraftAccount.h"
namespace Ui
{
@@ -32,7 +32,7 @@ class LoginDialog : public QDialog
public:
~LoginDialog();
- static MojangAccountPtr newAccount(QWidget *parent, QString message);
+ static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
private:
explicit LoginDialog(QWidget *parent = 0);
@@ -53,6 +53,6 @@ slots:
private:
Ui::LoginDialog *ui;
- MojangAccountPtr m_account;
+ MinecraftAccountPtr m_account;
std::shared_ptr<Task> m_loginTask;
};
diff --git a/launcher/dialogs/LoginDialog.ui b/launcher/dialogs/LoginDialog.ui
index dbdb3b93..8fa4a45d 100644
--- a/launcher/dialogs/LoginDialog.ui
+++ b/launcher/dialogs/LoginDialog.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>421</width>
- <height>238</height>
+ <height>198</height>
</rect>
</property>
<property name="sizePolicy">
@@ -21,16 +21,6 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QLabel" name="microsoftAccountsNoticeLabel">
- <property name="text">
- <string>NOTICE: MultiMC does not currently support Microsoft accounts. This means that accounts created from December 2020 onwards cannot be used.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp
new file mode 100644
index 00000000..778b379d
--- /dev/null
+++ b/launcher/dialogs/MSALoginDialog.cpp
@@ -0,0 +1,96 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MSALoginDialog.h"
+#include "ui_MSALoginDialog.h"
+
+#include "minecraft/auth/AccountTask.h"
+
+#include <QtWidgets/QPushButton>
+
+MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+ // ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+int MSALoginDialog::exec() {
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createBlankMSA();
+ m_loginTask = m_account->loginMSA(nullptr);
+ connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
+ m_loginTask->start();
+
+ return QDialog::exec();
+}
+
+
+MSALoginDialog::~MSALoginDialog()
+{
+ delete ui;
+}
+
+void MSALoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->buttonBox->setEnabled(enable);
+}
+
+void MSALoginDialog::onTaskFailed(const QString &reason)
+{
+ // Set message
+ ui->label->setText("<span style='color:red'>" + reason + "</span>");
+
+ // Re-enable user-interaction
+ setUserInputsEnabled(true);
+ ui->progressBar->setVisible(false);
+}
+
+void MSALoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void MSALoginDialog::onTaskStatus(const QString &status)
+{
+ ui->label->setText(status);
+}
+
+void MSALoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg)
+{
+ MSALoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return 0;
+}
diff --git a/launcher/dialogs/MSALoginDialog.h b/launcher/dialogs/MSALoginDialog.h
new file mode 100644
index 00000000..402180ee
--- /dev/null
+++ b/launcher/dialogs/MSALoginDialog.h
@@ -0,0 +1,55 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QtWidgets/QDialog>
+#include <QtCore/QEventLoop>
+
+#include "minecraft/auth/MinecraftAccount.h"
+
+namespace Ui
+{
+class MSALoginDialog;
+}
+
+class MSALoginDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ~MSALoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
+ int exec() override;
+
+private:
+ explicit MSALoginDialog(QWidget *parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+protected
+slots:
+ void onTaskFailed(const QString &reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString &status);
+ void onTaskProgress(qint64 current, qint64 total);
+
+private:
+ Ui::MSALoginDialog *ui;
+ MinecraftAccountPtr m_account;
+ std::shared_ptr<Task> m_loginTask;
+};
+
diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui
new file mode 100644
index 00000000..4ae8085a
--- /dev/null
+++ b/launcher/dialogs/MSALoginDialog.ui
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MSALoginDialog</class>
+ <widget class="QDialog" name="MSALoginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>421</width>
+ <height>114</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Add Microsoft Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/ProfileSelectDialog.cpp b/launcher/dialogs/ProfileSelectDialog.cpp
index ae34709f..e2ad73e4 100644
--- a/launcher/dialogs/ProfileSelectDialog.cpp
+++ b/launcher/dialogs/ProfileSelectDialog.cpp
@@ -33,9 +33,10 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid
m_accounts = MMC->accounts();
auto view = ui->listView;
//view->setModel(m_accounts.get());
- //view->hideColumn(MojangAccountList::ActiveColumn);
+ //view->hideColumn(AccountList::ActiveColumn);
view->setColumnCount(1);
view->setRootIsDecorated(false);
+ // FIXME: use a real model, not this
if(QTreeWidgetItem* header = view->headerItem())
{
header->setText(0, tr("Name"));
@@ -47,20 +48,19 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid
QList <QTreeWidgetItem *> items;
for (int i = 0; i < m_accounts->count(); i++)
{
- MojangAccountPtr account = m_accounts->at(i);
- for (auto profile : account->profiles())
- {
- auto profileLabel = profile.name;
- if(account->isInUse())
- {
- profileLabel += tr(" (in use)");
- }
- auto item = new QTreeWidgetItem(view);
- item->setText(0, profileLabel);
- item->setIcon(0, SkinUtils::getFaceFromCache(profile.id));
- item->setData(0, MojangAccountList::PointerRole, QVariant::fromValue(account));
- items.append(item);
+ MinecraftAccountPtr account = m_accounts->at(i);
+ QString profileLabel;
+ if(account->isInUse()) {
+ profileLabel = tr("%1 (in use)").arg(account->profileName());
}
+ else {
+ profileLabel = account->profileName();
+ }
+ auto item = new QTreeWidgetItem(view);
+ item->setText(0, profileLabel);
+ item->setIcon(0, account->getFace());
+ item->setData(0, AccountList::PointerRole, QVariant::fromValue(account));
+ items.append(item);
}
view->addTopLevelItems(items);
@@ -84,7 +84,7 @@ ProfileSelectDialog::~ProfileSelectDialog()
delete ui;
}
-MojangAccountPtr ProfileSelectDialog::selectedAccount() const
+MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const
{
return m_selected;
}
@@ -105,7 +105,7 @@ void ProfileSelectDialog::on_buttonBox_accepted()
if (selection.size() > 0)
{
QModelIndex selected = selection.first();
- m_selected = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ m_selected = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
}
close();
}
diff --git a/launcher/dialogs/ProfileSelectDialog.h b/launcher/dialogs/ProfileSelectDialog.h
index 9f95830c..a4acd9a1 100644
--- a/launcher/dialogs/ProfileSelectDialog.h
+++ b/launcher/dialogs/ProfileSelectDialog.h
@@ -19,7 +19,7 @@
#include <memory>
-#include "minecraft/auth/MojangAccountList.h"
+#include "minecraft/auth/AccountList.h"
namespace Ui
{
@@ -59,7 +59,7 @@ public:
* Gets a pointer to the account that the user selected.
* This is null if the user clicked cancel or hasn't clicked OK yet.
*/
- MojangAccountPtr selectedAccount() const;
+ MinecraftAccountPtr selectedAccount() const;
/*!
* Returns true if the user checked the "use as global default" checkbox.
@@ -80,10 +80,10 @@ slots:
void on_buttonBox_rejected();
protected:
- std::shared_ptr<MojangAccountList> m_accounts;
+ std::shared_ptr<AccountList> m_accounts;
//! The account that was selected when the user clicked OK.
- MojangAccountPtr m_selected;
+ MinecraftAccountPtr m_selected;
private:
Ui::ProfileSelectDialog *ui;
diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp
index 56133529..3c62edac 100644
--- a/launcher/dialogs/SkinUploadDialog.cpp
+++ b/launcher/dialogs/SkinUploadDialog.cpp
@@ -107,7 +107,7 @@ void SkinUploadDialog::on_skinBrowseBtn_clicked()
ui->skinPathTextBox->setText(cooked_path);
}
-SkinUploadDialog::SkinUploadDialog(MojangAccountPtr acct, QWidget *parent)
+SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent)
:QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
{
ui->setupUi(this);
diff --git a/launcher/dialogs/SkinUploadDialog.h b/launcher/dialogs/SkinUploadDialog.h
index deb44eac..84d17dc6 100644
--- a/launcher/dialogs/SkinUploadDialog.h
+++ b/launcher/dialogs/SkinUploadDialog.h
@@ -1,7 +1,7 @@
#pragma once
#include <QDialog>
-#include <minecraft/auth/MojangAccount.h>
+#include <minecraft/auth/MinecraftAccount.h>
namespace Ui
{
@@ -11,7 +11,7 @@ namespace Ui
class SkinUploadDialog : public QDialog {
Q_OBJECT
public:
- explicit SkinUploadDialog(MojangAccountPtr acct, QWidget *parent = 0);
+ explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent = 0);
virtual ~SkinUploadDialog() {};
public slots:
@@ -22,7 +22,7 @@ public slots:
void on_skinBrowseBtn_clicked();
protected:
- MojangAccountPtr m_acct;
+ MinecraftAccountPtr m_acct;
private:
Ui::SkinUploadDialog *ui;
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index dbf9f816..5f3c7244 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -423,7 +423,7 @@ QStringList MinecraftInstance::processMinecraftArgs(
// yggdrasil!
if(session)
{
- token_mapping["auth_username"] = session->username;
+ // token_mapping["auth_username"] = session->username;
token_mapping["auth_session"] = session->session;
token_mapping["auth_access_token"] = session->access_token;
token_mapping["auth_player_name"] = session->player_name;
@@ -691,19 +691,11 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
addToFilter(sessionRef.session, tr("<SESSION ID>"));
}
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
- addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
+ if(sessionRef.client_token.size()) {
+ addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
+ }
addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
- auto i = sessionRef.u.properties.begin();
- while (i != sessionRef.u.properties.end())
- {
- if(i.value().length() <= 3) {
- ++i;
- continue;
- }
- addToFilter(i.value(), "<" + i.key().toUpper() + ">");
- ++i;
- }
return filter;
}
diff --git a/launcher/minecraft/auth-msa/BuildConfig.cpp.in b/launcher/minecraft/auth-msa/BuildConfig.cpp.in
deleted file mode 100644
index 8f470e25..00000000
--- a/launcher/minecraft/auth-msa/BuildConfig.cpp.in
+++ /dev/null
@@ -1,9 +0,0 @@
-#include "BuildConfig.h"
-#include <QObject>
-
-const Config BuildConfig;
-
-Config::Config()
-{
- CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
-}
diff --git a/launcher/minecraft/auth-msa/BuildConfig.h b/launcher/minecraft/auth-msa/BuildConfig.h
deleted file mode 100644
index 7a01d704..00000000
--- a/launcher/minecraft/auth-msa/BuildConfig.h
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma once
-#include <QString>
-
-class Config
-{
-public:
- Config();
- QString CLIENT_ID;
-};
-
-extern const Config BuildConfig;
diff --git a/launcher/minecraft/auth-msa/CMakeLists.txt b/launcher/minecraft/auth-msa/CMakeLists.txt
deleted file mode 100644
index 22777d1b..00000000
--- a/launcher/minecraft/auth-msa/CMakeLists.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-find_package(Qt5 COMPONENTS Core Gui Network Widgets REQUIRED)
-
-set(CMAKE_AUTOMOC ON)
-set(CMAKE_AUTOUIC ON)
-set(CMAKE_INCLUDE_CURRENT_DIR ON)
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
-
-
-set(MOJANGDEMO_CLIENT_ID "" CACHE STRING "Client ID used for OAuth2 in mojangdemo")
-
-configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp")
-
-set(mojang_SRCS
- main.cpp
- context.cpp
- context.h
-
- mainwindow.cpp
- mainwindow.h
- mainwindow.ui
-
- ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp
- BuildConfig.h
-)
-
-add_executable( mojangdemo ${mojang_SRCS} )
-target_link_libraries( mojangdemo Katabasis Qt5::Gui Qt5::Widgets )
-target_include_directories(mojangdemo PRIVATE logic)
diff --git a/launcher/minecraft/auth-msa/main.cpp b/launcher/minecraft/auth-msa/main.cpp
deleted file mode 100644
index 481e0126..00000000
--- a/launcher/minecraft/auth-msa/main.cpp
+++ /dev/null
@@ -1,100 +0,0 @@
-#include <QApplication>
-#include <QStringList>
-#include <QTimer>
-#include <QDebug>
-#include <QFile>
-#include <QSaveFile>
-
-#include "context.h"
-#include "mainwindow.h"
-
-void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
-{
- QByteArray localMsg = msg.toLocal8Bit();
- const char *file = context.file ? context.file : "";
- const char *function = context.function ? context.function : "";
- switch (type) {
- case QtDebugMsg:
- fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
- break;
- case QtInfoMsg:
- fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
- break;
- case QtWarningMsg:
- fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
- break;
- case QtCriticalMsg:
- fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
- break;
- case QtFatalMsg:
- fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
- break;
- }
-}
-
-class Helper : public QObject {
- Q_OBJECT
-
-public:
- Helper(Context * context) : QObject(), context_(context), msg_(QString()) {
- QFile tokenCache("usercache.dat");
- if(tokenCache.open(QIODevice::ReadOnly)) {
- context_->resumeFromState(tokenCache.readAll());
- }
- }
-
-public slots:
- void run() {
- connect(context_, &Context::activityChanged, this, &Helper::onActivityChanged);
- context_->silentSignIn();
- }
-
- void onFailed() {
- qDebug() << "Login failed";
- }
-
- void onActivityChanged(Katabasis::Activity activity) {
- if(activity == Katabasis::Activity::Idle) {
- switch(context_->validity()) {
- case Katabasis::Validity::None: {
- // account is gone, remove it.
- QFile::remove("usercache.dat");
- }
- break;
- case Katabasis::Validity::Assumed: {
- // this is basically a soft-failed refresh. do nothing.
- }
- break;
- case Katabasis::Validity::Certain: {
- // stuff got refreshed / signed in. Save.
- auto data = context_->saveState();
- QSaveFile tokenCache("usercache.dat");
- if(tokenCache.open(QIODevice::WriteOnly)) {
- tokenCache.write(context_->saveState());
- tokenCache.commit();
- }
- }
- break;
- }
- }
- }
-
-private:
- Context *context_;
- QString msg_;
-};
-
-int main(int argc, char *argv[]) {
- qInstallMessageHandler(myMessageOutput);
- QApplication a(argc, argv);
- QCoreApplication::setOrganizationName("MultiMC");
- QCoreApplication::setApplicationName("MultiMC");
- Context c;
- Helper helper(&c);
- MainWindow window(&c);
- window.show();
- QTimer::singleShot(0, &helper, &Helper::run);
- return a.exec();
-}
-
-#include "main.moc"
diff --git a/launcher/minecraft/auth-msa/mainwindow.cpp b/launcher/minecraft/auth-msa/mainwindow.cpp
deleted file mode 100644
index d4e18dc0..00000000
--- a/launcher/minecraft/auth-msa/mainwindow.cpp
+++ /dev/null
@@ -1,97 +0,0 @@
-#include "mainwindow.h"
-#include "ui_mainwindow.h"
-#include <QDebug>
-
-#include <QDesktopServices>
-
-#include "BuildConfig.h"
-
-MainWindow::MainWindow(Context * context, QWidget *parent) :
- QMainWindow(parent),
- m_context(context),
- m_ui(new Ui::MainWindow)
-{
- m_ui->setupUi(this);
- connect(m_ui->signInButton_MSA, &QPushButton::clicked, this, &MainWindow::SignInMSAClicked);
- connect(m_ui->signInButton_Mojang, &QPushButton::clicked, this, &MainWindow::SignInMojangClicked);
- connect(m_ui->signOutButton, &QPushButton::clicked, this, &MainWindow::SignOutClicked);
- connect(m_ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshClicked);
-
- // connect(m_context, &Context::linkingSucceeded, this, &MainWindow::SignInSucceeded);
- // connect(m_context, &Context::linkingFailed, this, &MainWindow::SignInFailed);
- connect(m_context, &Context::activityChanged, this, &MainWindow::ActivityChanged);
- ActivityChanged(Katabasis::Activity::Idle);
-}
-
-MainWindow::~MainWindow() = default;
-
-void MainWindow::ActivityChanged(Katabasis::Activity activity) {
- switch(activity) {
- case Katabasis::Activity::Idle: {
- if(m_context->validity() != Katabasis::Validity::None) {
- m_ui->signInButton_Mojang->setEnabled(false);
- m_ui->signInButton_MSA->setEnabled(false);
- m_ui->signOutButton->setEnabled(true);
- m_ui->refreshButton->setEnabled(true);
- m_ui->statusBar->showMessage(QString("Hello %1!").arg(m_context->userName()));
- }
- else {
- m_ui->signInButton_Mojang->setEnabled(true);
- m_ui->signInButton_MSA->setEnabled(true);
- m_ui->signOutButton->setEnabled(false);
- m_ui->refreshButton->setEnabled(false);
- m_ui->statusBar->showMessage("Press the login button to start.");
- }
- }
- break;
- case Katabasis::Activity::LoggingIn: {
- m_ui->signInButton_Mojang->setEnabled(false);
- m_ui->signInButton_MSA->setEnabled(false);
- m_ui->signOutButton->setEnabled(false);
- m_ui->refreshButton->setEnabled(false);
- m_ui->statusBar->showMessage("Logging in...");
- }
- break;
- case Katabasis::Activity::LoggingOut: {
- m_ui->signInButton_Mojang->setEnabled(false);
- m_ui->signInButton_MSA->setEnabled(false);
- m_ui->signOutButton->setEnabled(false);
- m_ui->refreshButton->setEnabled(false);
- m_ui->statusBar->showMessage("Logging out...");
- }
- break;
- case Katabasis::Activity::Refreshing: {
- m_ui->signInButton_Mojang->setEnabled(false);
- m_ui->signInButton_MSA->setEnabled(false);
- m_ui->signOutButton->setEnabled(false);
- m_ui->refreshButton->setEnabled(false);
- m_ui->statusBar->showMessage("Refreshing login...");
- }
- break;
- }
-}
-
-void MainWindow::SignInMSAClicked() {
- qDebug() << "Sign In MSA";
- // signIn({{"prompt", "select_account"}})
- // FIXME: wrong. very wrong. this should not be operating on the current context
- m_context->signIn();
-}
-
-void MainWindow::SignInMojangClicked() {
- qDebug() << "Sign In Mojang";
- // signIn({{"prompt", "select_account"}})
- // FIXME: wrong. very wrong. this should not be operating on the current context
- m_context->signIn();
-}
-
-
-void MainWindow::SignOutClicked() {
- qDebug() << "Sign Out";
- m_context->signOut();
-}
-
-void MainWindow::RefreshClicked() {
- qDebug() << "Refresh";
- m_context->silentSignIn();
-}
diff --git a/launcher/minecraft/auth-msa/mainwindow.h b/launcher/minecraft/auth-msa/mainwindow.h
deleted file mode 100644
index abde52d8..00000000
--- a/launcher/minecraft/auth-msa/mainwindow.h
+++ /dev/null
@@ -1,34 +0,0 @@
-#pragma once
-
-#include <QMainWindow>
-#include <QScopedPointer>
-#include <QtNetwork>
-#include <katabasis/Bits.h>
-
-#include "context.h"
-
-namespace Ui {
-class MainWindow;
-}
-
-class MainWindow : public QMainWindow {
- Q_OBJECT
-
-public:
- explicit MainWindow(Context * context, QWidget *parent = nullptr);
- ~MainWindow() override;
-
-private slots:
- void SignInMojangClicked();
- void SignInMSAClicked();
-
- void SignOutClicked();
- void RefreshClicked();
-
- void ActivityChanged(Katabasis::Activity activity);
-
-private:
- Context* m_context;
- QScopedPointer<Ui::MainWindow> m_ui;
-};
-
diff --git a/launcher/minecraft/auth-msa/mainwindow.ui b/launcher/minecraft/auth-msa/mainwindow.ui
deleted file mode 100644
index 32b34128..00000000
--- a/launcher/minecraft/auth-msa/mainwindow.ui
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>MainWindow</class>
- <widget class="QMainWindow" name="MainWindow">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>1037</width>
- <height>511</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>SmartMapsClient</string>
- </property>
- <property name="dockNestingEnabled">
- <bool>true</bool>
- </property>
- <widget class="QWidget" name="centralWidget">
- <layout class="QGridLayout" name="gridLayout">
- <item row="1" column="3">
- <widget class="QPushButton" name="signInButton_Mojang">
- <property name="text">
- <string>SignIn Mojang</string>
- </property>
- </widget>
- </item>
- <item row="3" column="3">
- <widget class="Line" name="line">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- </widget>
- </item>
- <item row="1" column="0" rowspan="7" colspan="3">
- <widget class="QTreeView" name="accountView"/>
- </item>
- <item row="5" column="3">
- <widget class="QPushButton" name="refreshButton">
- <property name="text">
- <string>Refresh</string>
- </property>
- </widget>
- </item>
- <item row="2" column="3">
- <widget class="QPushButton" name="signInButton_MSA">
- <property name="text">
- <string>SignIn MSA</string>
- </property>
- </widget>
- </item>
- <item row="6" column="3">
- <widget class="QPushButton" name="signOutButton">
- <property name="text">
- <string>SignOut</string>
- </property>
- </widget>
- </item>
- <item row="4" column="3">
- <widget class="QPushButton" name="makeActiveButton">
- <property name="text">
- <string>Make Active</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QStatusBar" name="statusBar"/>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp
new file mode 100644
index 00000000..77c73c1b
--- /dev/null
+++ b/launcher/minecraft/auth/AccountData.cpp
@@ -0,0 +1,387 @@
+#include "AccountData.h"
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QDebug>
+#include <QUuid>
+
+namespace {
+void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
+ if(!t.persistent) {
+ return;
+ }
+ QJsonObject out;
+ if(t.issueInstant.isValid()) {
+ out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000);
+ }
+
+ if(t.notAfter.isValid()) {
+ out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000);
+ }
+
+ bool save = false;
+ if(!t.token.isEmpty()) {
+ out["token"] = QJsonValue(t.token);
+ save = true;
+ }
+ if(!t.refresh_token.isEmpty()) {
+ out["refresh_token"] = QJsonValue(t.refresh_token);
+ save = true;
+ }
+ if(t.extra.size()) {
+ out["extra"] = QJsonObject::fromVariantMap(t.extra);
+ save = true;
+ }
+ if(save) {
+ parent[tokenName] = out;
+ }
+}
+
+Katabasis::Token tokenFromJSONV3(const QJsonObject &parent, const char * tokenName) {
+ Katabasis::Token out;
+ auto tokenObject = parent.value(tokenName).toObject();
+ if(tokenObject.isEmpty()) {
+ return out;
+ }
+ auto issueInstant = tokenObject.value("iat");
+ if(issueInstant.isDouble()) {
+ out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t) issueInstant.toDouble()) * 1000);
+ }
+
+ auto notAfter = tokenObject.value("exp");
+ if(notAfter.isDouble()) {
+ out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t) notAfter.toDouble()) * 1000);
+ }
+
+ auto token = tokenObject.value("token");
+ if(token.isString()) {
+ out.token = token.toString();
+ out.validity = Katabasis::Validity::Assumed;
+ }
+
+ auto refresh_token = tokenObject.value("refresh_token");
+ if(refresh_token.isString()) {
+ out.refresh_token = refresh_token.toString();
+ }
+
+ auto extra = tokenObject.value("extra");
+ if(extra.isObject()) {
+ out.extra = extra.toObject().toVariantMap();
+ }
+ return out;
+}
+
+void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
+ if(p.id.isEmpty()) {
+ return;
+ }
+ QJsonObject out;
+ out["id"] = QJsonValue(p.id);
+ out["name"] = QJsonValue(p.name);
+ if(p.currentCape != -1) {
+ out["cape"] = p.capes[p.currentCape].id;
+ }
+
+ {
+ QJsonObject skinObj;
+ skinObj["id"] = p.skin.id;
+ skinObj["url"] = p.skin.url;
+ skinObj["variant"] = p.skin.variant;
+ if(p.skin.data.size()) {
+ skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
+ }
+ out["skin"] = skinObj;
+ }
+
+ QJsonArray capesArray;
+ for(auto & cape: p.capes) {
+ QJsonObject capeObj;
+ capeObj["id"] = cape.id;
+ capeObj["url"] = cape.url;
+ capeObj["alias"] = cape.alias;
+ if(cape.data.size()) {
+ capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
+ }
+ capesArray.push_back(capeObj);
+ }
+ out["capes"] = capesArray;
+ parent[tokenName] = out;
+}
+
+MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * tokenName) {
+ MinecraftProfile out;
+ auto tokenObject = parent.value(tokenName).toObject();
+ if(tokenObject.isEmpty()) {
+ return out;
+ }
+ {
+ auto idV = tokenObject.value("id");
+ auto nameV = tokenObject.value("name");
+ if(!idV.isString() || !nameV.isString()) {
+ qWarning() << "mandatory profile attributes are missing or of unexpected type";
+ return MinecraftProfile();
+ }
+ out.name = nameV.toString();
+ out.id = idV.toString();
+ }
+
+ {
+ auto skinV = tokenObject.value("skin");
+ if(!skinV.isObject()) {
+ qWarning() << "skin is missing";
+ return MinecraftProfile();
+ }
+ auto skinObj = skinV.toObject();
+ auto idV = skinObj.value("id");
+ auto urlV = skinObj.value("url");
+ auto variantV = skinObj.value("variant");
+ if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
+ qWarning() << "mandatory skin attributes are missing or of unexpected type";
+ return MinecraftProfile();
+ }
+ out.skin.id = idV.toString();
+ out.skin.url = urlV.toString();
+ out.skin.variant = variantV.toString();
+
+ // data for skin is optional
+ auto dataV = skinObj.value("data");
+ if(dataV.isString()) {
+ // TODO: validate base64
+ out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
+ }
+ else if (!dataV.isUndefined()) {
+ qWarning() << "skin data is something unexpected";
+ return MinecraftProfile();
+ }
+ }
+
+ auto capesV = tokenObject.value("capes");
+ if(!capesV.isArray()) {
+ qWarning() << "capes is not an array!";
+ return MinecraftProfile();
+ }
+ auto capesArray = capesV.toArray();
+ for(auto capeV: capesArray) {
+ if(!capeV.isObject()) {
+ qWarning() << "cape is not an object!";
+ return MinecraftProfile();
+ }
+ auto capeObj = capeV.toObject();
+ auto idV = capeObj.value("id");
+ auto urlV = capeObj.value("url");
+ auto aliasV = capeObj.value("alias");
+ if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
+ qWarning() << "mandatory skin attributes are missing or of unexpected type";
+ return MinecraftProfile();
+ }
+ Cape cape;
+ cape.id = idV.toString();
+ cape.url = urlV.toString();
+ cape.alias = aliasV.toString();
+
+ // data for cape is optional.
+ auto dataV = capeObj.value("data");
+ if(dataV.isString()) {
+ // TODO: validate base64
+ cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
+ }
+ else if (!dataV.isUndefined()) {
+ qWarning() << "cape data is something unexpected";
+ return MinecraftProfile();
+ }
+ out.capes.push_back(cape);
+ }
+ out.validity = Katabasis::Validity::Assumed;
+ return out;
+}
+
+}
+
+bool AccountData::resumeStateFromV2(QJsonObject data) {
+ // The JSON object must at least have a username for it to be valid.
+ if (!data.value("username").isString())
+ {
+ qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type.";
+ return false;
+ }
+
+ QString userName = data.value("username").toString("");
+ QString clientToken = data.value("clientToken").toString("");
+ QString accessToken = data.value("accessToken").toString("");
+
+ QJsonArray profileArray = data.value("profiles").toArray();
+ if (profileArray.size() < 1)
+ {
+ qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found.";
+ return false;
+ }
+
+ struct AccountProfile
+ {
+ QString id;
+ QString name;
+ bool legacy;
+ };
+
+ QList<AccountProfile> profiles;
+ int currentProfileIndex = 0;
+ int index = -1;
+ QString currentProfile = data.value("activeProfile").toString("");
+ for (QJsonValue profileVal : profileArray)
+ {
+ index++;
+ QJsonObject profileObject = profileVal.toObject();
+ QString id = profileObject.value("id").toString("");
+ QString name = profileObject.value("name").toString("");
+ bool legacy = profileObject.value("legacy").toBool(false);
+ if (id.isEmpty() || name.isEmpty())
+ {
+ qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name.";
+ continue;
+ }
+ if(id == currentProfile) {
+ currentProfileIndex = index;
+ }
+ profiles.append({id, name, legacy});
+ }
+ auto & profile = profiles[currentProfileIndex];
+
+ type = AccountType::Mojang;
+ legacy = profile.legacy;
+
+ minecraftProfile.id = profile.id;
+ minecraftProfile.name = profile.name;
+ minecraftProfile.validity = Katabasis::Validity::Assumed;
+
+ yggdrasilToken.token = accessToken;
+ yggdrasilToken.extra["clientToken"] = clientToken;
+ yggdrasilToken.extra["userName"] = userName;
+ yggdrasilToken.validity = Katabasis::Validity::Assumed;
+
+ validity_ = minecraftProfile.validity;
+ return true;
+}
+
+bool AccountData::resumeStateFromV3(QJsonObject data) {
+ auto typeV = data.value("type");
+ if(!typeV.isString()) {
+ qWarning() << "Failed to parse account data: type is missing.";
+ return false;
+ }
+ auto typeS = typeV.toString();
+ if(typeS == "MSA") {
+ type = AccountType::MSA;
+ } else if (typeS == "Mojang") {
+ type = AccountType::Mojang;
+ } else {
+ qWarning() << "Failed to parse account data: type is not recognized.";
+ return false;
+ }
+
+ if(type == AccountType::Mojang) {
+ legacy = data.value("legacy").toBool(false);
+ canMigrateToMSA = data.value("canMigrateToMSA").toBool(false);
+ }
+
+ if(type == AccountType::MSA) {
+ msaToken = tokenFromJSONV3(data, "msa");
+ userToken = tokenFromJSONV3(data, "utoken");
+ xboxApiToken = tokenFromJSONV3(data, "xrp-main");
+ mojangservicesToken = tokenFromJSONV3(data, "xrp-mc");
+ }
+
+ yggdrasilToken = tokenFromJSONV3(data, "ygg");
+ minecraftProfile = profileFromJSONV3(data, "profile");
+
+ validity_ = minecraftProfile.validity;
+
+ return true;
+}
+
+QJsonObject AccountData::saveState() const {
+ QJsonObject output;
+ if(type == AccountType::Mojang) {
+ output["type"] = "Mojang";
+ if(legacy) {
+ output["legacy"] = true;
+ }
+ if(canMigrateToMSA) {
+ output["canMigrateToMSA"] = true;
+ }
+ }
+ else if (type == AccountType::MSA) {
+ output["type"] = "MSA";
+ tokenToJSONV3(output, msaToken, "msa");
+ tokenToJSONV3(output, userToken, "utoken");
+ tokenToJSONV3(output, xboxApiToken, "xrp-main");
+ tokenToJSONV3(output, mojangservicesToken, "xrp-mc");
+ }
+
+ tokenToJSONV3(output, yggdrasilToken, "ygg");
+ profileToJSONV3(output, minecraftProfile, "profile");
+ return output;
+}
+
+QString AccountData::userName() const {
+ if(type != AccountType::Mojang) {
+ return QString();
+ }
+ return yggdrasilToken.extra["userName"].toString();
+}
+
+QString AccountData::accessToken() const {
+ return yggdrasilToken.token;
+}
+
+QString AccountData::clientToken() const {
+ if(type != AccountType::Mojang) {
+ return QString();
+ }
+ return yggdrasilToken.extra["clientToken"].toString();
+}
+
+void AccountData::setClientToken(QString clientToken) {
+ if(type != AccountType::Mojang) {
+ return;
+ }
+ yggdrasilToken.extra["clientToken"] = clientToken;
+}
+
+void AccountData::generateClientTokenIfMissing() {
+ if(yggdrasilToken.extra.contains("clientToken")) {
+ return;
+ }
+ invalidateClientToken();
+}
+
+void AccountData::invalidateClientToken() {
+ if(type != AccountType::Mojang) {
+ return;
+ }
+ yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]"));
+}
+
+QString AccountData::profileId() const {
+ return minecraftProfile.id;
+}
+
+QString AccountData::profileName() const {
+ return minecraftProfile.name;
+}
+
+QString AccountData::accountDisplayString() const {
+ switch(type) {
+ case AccountType::Mojang: {
+ return userName();
+ }
+ case AccountType::MSA: {
+ if(xboxApiToken.extra.contains("gtg")) {
+ return xboxApiToken.extra["gtg"].toString();
+ }
+ return "Xbox profile missing";
+ }
+ default: {
+ return "Invalid Account";
+ }
+ }
+}
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h
new file mode 100644
index 00000000..b2d09cb0
--- /dev/null
+++ b/launcher/minecraft/auth/AccountData.h
@@ -0,0 +1,73 @@
+#pragma once
+#include <QString>
+#include <QByteArray>
+#include <QVector>
+#include <katabasis/Bits.h>
+#include <QJsonObject>
+
+struct Skin {
+ QString id;
+ QString url;
+ QString variant;
+
+ QByteArray data;
+};
+
+struct Cape {
+ QString id;
+ QString url;
+ QString alias;
+
+ QByteArray data;
+};
+
+struct MinecraftProfile {
+ QString id;
+ QString name;
+ Skin skin;
+ int currentCape = -1;
+ QVector<Cape> capes;
+ Katabasis::Validity validity = Katabasis::Validity::None;
+};
+
+enum class AccountType {
+ MSA,
+ Mojang
+};
+
+struct AccountData {
+ QJsonObject saveState() const;
+ bool resumeStateFromV2(QJsonObject data);
+ bool resumeStateFromV3(QJsonObject data);
+
+ //! userName for Mojang accounts, gamertag for MSA
+ QString accountDisplayString() const;
+
+ //! Only valid for Mojang accounts. MSA does not preserve this information
+ QString userName() const;
+
+ //! Only valid for Mojang accounts.
+ QString clientToken() const;
+ void setClientToken(QString clientToken);
+ void invalidateClientToken();
+ void generateClientTokenIfMissing();
+
+ //! Yggdrasil access token, as passed to the game.
+ QString accessToken() const;
+
+ QString profileId() const;
+ QString profileName() const;
+
+ AccountType type = AccountType::MSA;
+ bool legacy = false;
+ bool canMigrateToMSA = false;
+
+ Katabasis::Token msaToken;
+ Katabasis::Token userToken;
+ Katabasis::Token xboxApiToken;
+ Katabasis::Token mojangservicesToken;
+
+ Katabasis::Token yggdrasilToken;
+ MinecraftProfile minecraftProfile;
+ Katabasis::Validity validity_ = Katabasis::Validity::None;
+};
diff --git a/launcher/minecraft/auth/MojangAccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index e584cb3b..59028b60 100644
--- a/launcher/minecraft/auth/MojangAccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -13,8 +13,8 @@
* limitations under the License.
*/
-#include "MojangAccountList.h"
-#include "MojangAccount.h"
+#include "AccountList.h"
+#include "AccountData.h"
#include <QIODevice>
#include <QFile>
@@ -28,31 +28,49 @@
#include <QDebug>
#include <FileSystem.h>
+#include <QSaveFile>
-#define ACCOUNT_LIST_FORMAT_VERSION 2
+enum AccountListVersion {
+ MojangOnly = 2,
+ MojangMSA = 3
+};
-MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent)
-{
-}
+AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { }
-MojangAccountPtr MojangAccountList::findAccount(const QString &username) const
-{
- for (int i = 0; i < count(); i++)
- {
- MojangAccountPtr account = at(i);
- if (account->username() == username)
- return account;
+int AccountList::findAccountByProfileId(const QString& profileId) const {
+ for (int i = 0; i < count(); i++) {
+ MinecraftAccountPtr account = at(i);
+ if (account->profileId() == profileId) {
+ return i;
+ }
}
- return nullptr;
+ return -1;
}
-const MojangAccountPtr MojangAccountList::at(int i) const
+const MinecraftAccountPtr AccountList::at(int i) const
{
- return MojangAccountPtr(m_accounts.at(i));
+ return MinecraftAccountPtr(m_accounts.at(i));
}
-void MojangAccountList::addAccount(const MojangAccountPtr account)
+void AccountList::addAccount(const MinecraftAccountPtr account)
{
+ // We only ever want accounts with valid profiles.
+ // Keeping profile-less accounts is pointless and serves no purpose.
+ auto profileId = account->profileId();
+ if(!profileId.size()) {
+ return;
+ }
+
+ // override/replace existing account with the same profileId
+ auto existingAccount = findAccountByProfileId(profileId);
+ if(existingAccount != -1) {
+ m_accounts[existingAccount] = account;
+ emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1));
+ onListChanged();
+ return;
+ }
+
+ // if we don't have this porfileId yet, add the account to the end
int row = m_accounts.count();
beginInsertRows(QModelIndex(), row, row);
connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
@@ -61,24 +79,7 @@ void MojangAccountList::addAccount(const MojangAccountPtr account)
onListChanged();
}
-void MojangAccountList::removeAccount(const QString &username)
-{
- int idx = 0;
- for (auto account : m_accounts)
- {
- if (account->username() == username)
- {
- beginRemoveRows(QModelIndex(), idx, idx);
- m_accounts.removeOne(account);
- endRemoveRows();
- return;
- }
- idx++;
- }
- onListChanged();
-}
-
-void MojangAccountList::removeAccount(QModelIndex index)
+void AccountList::removeAccount(QModelIndex index)
{
int row = index.row();
if(index.isValid() && row >= 0 && row < m_accounts.size())
@@ -96,19 +97,19 @@ void MojangAccountList::removeAccount(QModelIndex index)
}
}
-MojangAccountPtr MojangAccountList::activeAccount() const
+MinecraftAccountPtr AccountList::activeAccount() const
{
return m_activeAccount;
}
-void MojangAccountList::setActiveAccount(const QString &username)
+void AccountList::setActiveAccount(const QString &profileId)
{
- if (username.isEmpty() && m_activeAccount)
+ if (profileId.isEmpty() && m_activeAccount)
{
int idx = 0;
auto prevActiveAcc = m_activeAccount;
m_activeAccount = nullptr;
- for (MojangAccountPtr account : m_accounts)
+ for (MinecraftAccountPtr account : m_accounts)
{
if (account == prevActiveAcc)
{
@@ -125,9 +126,9 @@ void MojangAccountList::setActiveAccount(const QString &username)
auto newActiveAccount = m_activeAccount;
int newActiveAccountIdx = -1;
int idx = 0;
- for (MojangAccountPtr account : m_accounts)
+ for (MinecraftAccountPtr account : m_accounts)
{
- if (account->username() == username)
+ if (account->profileId() == profileId)
{
newActiveAccount = account;
newActiveAccountIdx = idx;
@@ -148,13 +149,13 @@ void MojangAccountList::setActiveAccount(const QString &username)
}
}
-void MojangAccountList::accountChanged()
+void AccountList::accountChanged()
{
// the list changed. there is no doubt.
onListChanged();
}
-void MojangAccountList::onListChanged()
+void AccountList::onListChanged()
{
if (m_autosave)
// TODO: Alert the user if this fails.
@@ -163,7 +164,7 @@ void MojangAccountList::onListChanged()
emit listChanged();
}
-void MojangAccountList::onActiveChanged()
+void AccountList::onActiveChanged()
{
if (m_autosave)
saveList();
@@ -171,12 +172,12 @@ void MojangAccountList::onActiveChanged()
emit activeAccountChanged();
}
-int MojangAccountList::count() const
+int AccountList::count() const
{
return m_accounts.count();
}
-QVariant MojangAccountList::data(const QModelIndex &index, int role) const
+QVariant AccountList::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
@@ -184,51 +185,61 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
if (index.row() > count())
return QVariant();
- MojangAccountPtr account = at(index.row());
+ MinecraftAccountPtr account = at(index.row());
switch (role)
{
- case Qt::DisplayRole:
- switch (index.column())
- {
- case NameColumn:
- return account->username();
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case NameColumn:
+ return account->accountDisplayString();
- default:
- return QVariant();
- }
+ case TypeColumn: {
+ auto typeStr = account->typeString();
+ typeStr[0] = typeStr[0].toUpper();
+ return typeStr;
+ }
- case Qt::ToolTipRole:
- return account->username();
+ case ProfileNameColumn: {
+ return account->profileName();
+ }
- case PointerRole:
- return qVariantFromValue(account);
+ default:
+ return QVariant();
+ }
- case Qt::CheckStateRole:
- switch (index.column())
- {
- case ActiveColumn:
- return account == m_activeAccount ? Qt::Checked : Qt::Unchecked;
- }
+ case Qt::ToolTipRole:
+ return account->accountDisplayString();
- default:
- return QVariant();
+ case PointerRole:
+ return qVariantFromValue(account);
+
+ case Qt::CheckStateRole:
+ switch (index.column())
+ {
+ case NameColumn:
+ return account == m_activeAccount ? Qt::Checked : Qt::Unchecked;
+ }
+
+ default:
+ return QVariant();
}
}
-QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const
+QVariant AccountList::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
{
case Qt::DisplayRole:
switch (section)
{
- case ActiveColumn:
- return tr("Active?");
-
case NameColumn:
- return tr("Name");
-
+ return tr("Account");
+ case TypeColumn:
+ return tr("Type");
+ case ProfileNameColumn:
+ return tr("Profile");
default:
return QVariant();
}
@@ -237,8 +248,11 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation,
switch (section)
{
case NameColumn:
- return tr("The name of the version.");
-
+ return tr("User name of the account.");
+ case TypeColumn:
+ return tr("Type of the account - Mojang or MSA.");
+ case ProfileNameColumn:
+ return tr("Name of the Minecraft profile associated with the account.");
default:
return QVariant();
}
@@ -248,18 +262,18 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation,
}
}
-int MojangAccountList::rowCount(const QModelIndex &) const
+int AccountList::rowCount(const QModelIndex &) const
{
// Return count
return count();
}
-int MojangAccountList::columnCount(const QModelIndex &) const
+int AccountList::columnCount(const QModelIndex &) const
{
- return 2;
+ return NUM_COLUMNS;
}
-Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
+Qt::ItemFlags AccountList::flags(const QModelIndex &index) const
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
{
@@ -269,7 +283,7 @@ Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
-bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role)
+bool AccountList::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
{
@@ -280,8 +294,8 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value,
{
if(value == Qt::Checked)
{
- MojangAccountPtr account = this->at(index.row());
- this->setActiveAccount(account->username());
+ MinecraftAccountPtr account = at(index.row());
+ setActiveAccount(account->profileId());
}
}
@@ -289,31 +303,21 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value,
return true;
}
-void MojangAccountList::updateListData(QList<MojangAccountPtr> versions)
-{
- beginResetModel();
- m_accounts = versions;
- endResetModel();
-}
-
-bool MojangAccountList::loadList(const QString &filePath)
+bool AccountList::loadList()
{
- QString path = filePath;
- if (path.isEmpty())
- path = m_listFilePath;
- if (path.isEmpty())
+ if (m_listFilePath.isEmpty())
{
qCritical() << "Can't load Mojang account list. No file path given and no default set.";
return false;
}
- QFile file(path);
+ QFile file(m_listFilePath);
// Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user.
if (!file.open(QIODevice::ReadOnly))
{
- qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
+ qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8();
return false;
}
@@ -343,121 +347,168 @@ bool MojangAccountList::loadList(const QString &filePath)
QJsonObject root = jsonDoc.object();
// Make sure the format version matches.
- if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION)
- {
- QString newName = "accounts-old.json";
- qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to"
- << newName;
+ auto listVersion = root.value("formatVersion").toVariant().toInt();
+ switch(listVersion) {
+ case AccountListVersion::MojangOnly: {
+ return loadV2(root);
+ }
+ break;
+ case AccountListVersion::MojangMSA: {
+ return loadV3(root);
+ }
+ break;
+ default: {
+ QString newName = "accounts-old.json";
+ qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName;
+ // Attempt to rename the old version.
+ file.rename(newName);
+ return false;
+ }
+ }
+}
- // Attempt to rename the old version.
- file.rename(newName);
- return false;
+bool AccountList::loadV2(QJsonObject& root) {
+ beginResetModel();
+ auto activeUserName = root.value("activeAccount").toString("");
+ QJsonArray accounts = root.value("accounts").toArray();
+ for (QJsonValue accountVal : accounts)
+ {
+ QJsonObject accountObj = accountVal.toObject();
+ MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj);
+ if (account.get() != nullptr)
+ {
+ auto profileId = account->profileId();
+ if(!profileId.size()) {
+ continue;
+ }
+ if(findAccountByProfileId(profileId) != -1) {
+ continue;
+ }
+ connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
+ m_accounts.append(account);
+ if (activeUserName.size() && account->mojangUserName() == activeUserName) {
+ m_activeAccount = account;
+ }
+ }
+ else
+ {
+ qWarning() << "Failed to load an account.";
+ }
}
+ endResetModel();
+ return true;
+}
- // Now, load the accounts array.
+bool AccountList::loadV3(QJsonObject& root) {
beginResetModel();
QJsonArray accounts = root.value("accounts").toArray();
for (QJsonValue accountVal : accounts)
{
QJsonObject accountObj = accountVal.toObject();
- MojangAccountPtr account = MojangAccount::loadFromJson(accountObj);
+ MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj);
if (account.get() != nullptr)
{
- connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
+ auto profileId = account->profileId();
+ if(!profileId.size()) {
+ continue;
+ }
+ if(findAccountByProfileId(profileId) != -1) {
+ continue;
+ }
+ connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
m_accounts.append(account);
+ if(accountObj.value("active").toBool(false)) {
+ m_activeAccount = account;
+ }
}
else
{
qWarning() << "Failed to load an account.";
}
}
- // Load the active account.
- m_activeAccount = findAccount(root.value("activeAccount").toString(""));
endResetModel();
return true;
}
-bool MojangAccountList::saveList(const QString &filePath)
+
+bool AccountList::saveList()
{
- QString path(filePath);
- if (path.isEmpty())
- path = m_listFilePath;
- if (path.isEmpty())
+ if (m_listFilePath.isEmpty())
{
qCritical() << "Can't save Mojang account list. No file path given and no default set.";
return false;
}
// make sure the parent folder exists
- if(!FS::ensureFilePathExists(path))
+ if(!FS::ensureFilePathExists(m_listFilePath))
return false;
// make sure the file wasn't overwritten with a folder before (fixes a bug)
- QFileInfo finfo(path);
+ QFileInfo finfo(m_listFilePath);
if(finfo.isDir())
{
- QDir badDir(path);
+ QDir badDir(m_listFilePath);
badDir.removeRecursively();
}
- qDebug() << "Writing account list to" << path;
+ qDebug() << "Writing account list to" << m_listFilePath;
qDebug() << "Building JSON data structure.";
// Build the JSON document to write to the list file.
QJsonObject root;
- root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION);
+ root.insert("formatVersion", AccountListVersion::MojangMSA);
// Build a list of accounts.
qDebug() << "Building account array.";
QJsonArray accounts;
- for (MojangAccountPtr account : m_accounts)
+ for (MinecraftAccountPtr account : m_accounts)
{
QJsonObject accountObj = account->saveToJson();
+ if(m_activeAccount == account) {
+ accountObj["active"] = true;
+ }
accounts.append(accountObj);
}
// Insert the account list into the root object.
root.insert("accounts", accounts);
- if(m_activeAccount)
- {
- // Save the active account.
- root.insert("activeAccount", m_activeAccount->username());
- }
-
// Create a JSON document object to convert our JSON to bytes.
QJsonDocument doc(root);
// Now that we're done building the JSON object, we can write it to the file.
qDebug() << "Writing account list to file.";
- QFile file(path);
+ QSaveFile file(m_listFilePath);
// Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user.
if (!file.open(QIODevice::WriteOnly))
{
- qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
+ qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8();
return false;
}
// Write the JSON to the file.
file.write(doc.toJson());
file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
- file.close();
-
- qDebug() << "Saved account list to" << path;
-
- return true;
+ if(file.commit()) {
+ qDebug() << "Saved account list to" << m_listFilePath;
+ return true;
+ }
+ else {
+ qDebug() << "Failed to save accounts to" << m_listFilePath;
+ return false;
+ }
}
-void MojangAccountList::setListFilePath(QString path, bool autosave)
+void AccountList::setListFilePath(QString path, bool autosave)
{
m_listFilePath = path;
m_autosave = autosave;
}
-bool MojangAccountList::anyAccountIsValid()
+bool AccountList::anyAccountIsValid()
{
for(auto account:m_accounts)
{
diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h
new file mode 100644
index 00000000..ac3684ee
--- /dev/null
+++ b/launcher/minecraft/auth/AccountList.h
@@ -0,0 +1,118 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "MinecraftAccount.h"
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+/*!
+ * List of available Mojang accounts.
+ * This should be loaded in the background by MultiMC on startup.
+ */
+class AccountList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ModelRoles
+ {
+ PointerRole = 0x34B1CB48
+ };
+
+ enum VListColumns
+ {
+ // TODO: Add icon column.
+ NameColumn = 0,
+ ProfileNameColumn,
+ TypeColumn,
+
+ NUM_COLUMNS
+ };
+
+ explicit AccountList(QObject *parent = 0);
+
+ const MinecraftAccountPtr at(int i) const;
+ int count() const;
+
+ //////// List Model Functions ////////
+ QVariant data(const QModelIndex &index, int role) const override;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ virtual int rowCount(const QModelIndex &parent) const override;
+ virtual int columnCount(const QModelIndex &parent) const override;
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+
+ void addAccount(const MinecraftAccountPtr account);
+ void removeAccount(QModelIndex index);
+ int findAccountByProfileId(const QString &profileId) const;
+
+ /*!
+ * 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.
+ * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
+ * after calling this function to ensure an autosaved change doesn't overwrite the list you intended
+ * to load.
+ */
+ void setListFilePath(QString path, bool autosave = false);
+
+ bool loadList();
+ bool loadV2(QJsonObject &root);
+ bool loadV3(QJsonObject &root);
+ bool saveList();
+
+ MinecraftAccountPtr activeAccount() const;
+ void setActiveAccount(const QString &profileId);
+ bool anyAccountIsValid();
+
+signals:
+ void listChanged();
+ void activeAccountChanged();
+
+public slots:
+ /**
+ * This is called when one of the accounts changes and the list needs to be updated
+ */
+ void accountChanged();
+
+protected:
+ /*!
+ * Called whenever the list changes.
+ * This emits the listChanged() signal and autosaves the list (if autosave is enabled).
+ */
+ void onListChanged();
+
+ /*!
+ * Called whenever the active account changes.
+ * Emits the activeAccountChanged() signal and autosaves the list if enabled.
+ */
+ void onActiveChanged();
+
+ QList<MinecraftAccountPtr> m_accounts;
+
+ MinecraftAccountPtr m_activeAccount;
+
+ //! Path to the account list file. Empty string if there isn't one.
+ QString m_listFilePath;
+
+ /*!
+ * If true, the account list will automatically save to the account list path when it changes.
+ * Ignored if m_listFilePath is blank.
+ */
+ bool m_autosave = false;
+};
diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp
new file mode 100644
index 00000000..c06be42b
--- /dev/null
+++ b/launcher/minecraft/auth/AccountTask.cpp
@@ -0,0 +1,69 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AccountTask.h"
+#include "MinecraftAccount.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QByteArray>
+
+#include <Env.h>
+
+#include <BuildConfig.h>
+
+#include <QDebug>
+
+AccountTask::AccountTask(AccountData *data, QObject *parent)
+ : Task(parent), m_data(data)
+{
+ changeState(STATE_CREATED);
+}
+
+QString AccountTask::getStateMessage() const
+{
+ switch (m_accountState)
+ {
+ case STATE_CREATED:
+ return "Waiting...";
+ case STATE_WORKING:
+ return tr("Sending request to auth servers...");
+ case STATE_SUCCEEDED:
+ return tr("Authentication task succeeded.");
+ case STATE_FAILED_SOFT:
+ return tr("Failed to contact the authentication server.");
+ case STATE_FAILED_HARD:
+ return tr("Failed to authenticate.");
+ default:
+ return tr("...");
+ }
+}
+
+void AccountTask::changeState(AccountTask::State newState, QString reason)
+{
+ m_accountState = newState;
+ setStatus(getStateMessage());
+ if (newState == STATE_SUCCEEDED)
+ {
+ emitSucceeded();
+ }
+ else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
+ {
+ emitFailed(reason);
+ }
+}
diff --git a/launcher/minecraft/auth/YggdrasilTask.h b/launcher/minecraft/auth/AccountTask.h
index 8af2e132..3f08096f 100644
--- a/launcher/minecraft/auth/YggdrasilTask.h
+++ b/launcher/minecraft/auth/AccountTask.h
@@ -22,19 +22,17 @@
#include <QTimer>
#include <qsslerror.h>
-#include "MojangAccount.h"
+#include "MinecraftAccount.h"
class QNetworkReply;
-/**
- * A Yggdrasil task is a task that performs an operation on a given mojang account.
- */
-class YggdrasilTask : public Task
+class AccountTask : public Task
{
+ friend class AuthContext;
Q_OBJECT
public:
- explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0);
- virtual ~YggdrasilTask() {};
+ explicit AccountTask(AccountData * data, QObject *parent = 0);
+ virtual ~AccountTask() {};
/**
* assign a session to this task. the session will be filled with required infomration
@@ -52,7 +50,7 @@ public:
}
/**
- * Class describing a Yggdrasil error response.
+ * Class describing a Account error response.
*/
struct Error
{
@@ -75,45 +73,17 @@ public:
enum State
{
STATE_CREATED,
- STATE_SENDING_REQUEST,
- STATE_PROCESSING_RESPONSE,
+ 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_SUCCEEDED
- } m_state = STATE_CREATED;
-
-protected:
-
- virtual void executeTask() override;
-
- /**
- * Gets the JSON object that will be sent to the authentication server.
- * Should be overridden by subclasses.
- */
- virtual QJsonObject getRequestContent() const = 0;
-
- /**
- * Gets the endpoint to POST to.
- * No leading slash.
- */
- virtual QString getEndpoint() const = 0;
+ } m_accountState = STATE_CREATED;
- /**
- * Processes the response received from the server.
- * If an error occurred, this should emit a failed signal and return false.
- * If Yggdrasil gave an error response, it should call setError() first, and then return false.
- * Otherwise, it should return true.
- * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
- * an empty QJsonObject.
- */
- virtual void processResponse(QJsonObject responseData) = 0;
+ State accountState() {
+ return m_accountState;
+ }
- /**
- * Processes an error response received from the server.
- * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
- * \returns a QString error message that will be passed to emitFailed.
- */
- virtual void processError(QJsonObject responseData);
+protected:
/**
* Returns the state message for the given state.
@@ -122,30 +92,12 @@ protected:
*/
virtual QString getStateMessage() const;
-protected
-slots:
- void processReply();
- void refreshTimers(qint64, qint64);
- void heartbeat();
- void sslErrors(QList<QSslError>);
-
+protected slots:
void changeState(State newState, QString reason=QString());
-public
-slots:
- virtual bool abort() override;
- void abortByTimeout();
- State state();
+
protected:
// FIXME: segfault disaster waiting to happen
- MojangAccount *m_account = nullptr;
- QNetworkReply *m_netReply = nullptr;
+ AccountData *m_data = nullptr;
std::shared_ptr<Error> m_error;
- QTimer timeout_keeper;
- QTimer counter;
- int count = 0; // num msec since time reset
-
- const int timeout_max = 30000;
- const int time_step = 50;
-
AuthSessionPtr m_session;
};
diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp
index 4e858796..d44f9098 100644
--- a/launcher/minecraft/auth/AuthSession.cpp
+++ b/launcher/minecraft/auth/AuthSession.cpp
@@ -7,11 +7,13 @@
QString AuthSession::serializeUserProperties()
{
QJsonObject userAttrs;
+ /*
for (auto key : u.properties.keys())
{
auto array = QJsonArray::fromStringList(u.properties.values(key));
userAttrs.insert(key, array);
}
+ */
QJsonDocument value(userAttrs);
return value.toJson(QJsonDocument::Compact);
diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h
index 29958597..d77435b8 100644
--- a/launcher/minecraft/auth/AuthSession.h
+++ b/launcher/minecraft/auth/AuthSession.h
@@ -4,13 +4,7 @@
#include <QMultiMap>
#include <memory>
-class MojangAccount;
-
-struct User
-{
- QString id;
- QMultiMap<QString, QString> properties;
-};
+class MinecraftAccount;
struct AuthSession
{
@@ -21,13 +15,12 @@ struct AuthSession
enum Status
{
Undetermined,
+ RequiresOAuth,
RequiresPassword,
PlayableOffline,
PlayableOnline
} status = Undetermined;
- User u;
-
// client token
QString client_token;
// account user name
@@ -46,7 +39,7 @@ struct AuthSession
bool auth_server_online = false;
// Did the user request online mode?
bool wants_online = true;
- std::shared_ptr<MojangAccount> m_accountPtr;
+ std::shared_ptr<MinecraftAccount> m_accountPtr;
};
typedef std::shared_ptr<AuthSession> AuthSessionPtr;
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
new file mode 100644
index 00000000..671f9c38
--- /dev/null
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -0,0 +1,303 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MinecraftAccount.h"
+#include "flows/AuthContext.h"
+
+#include <QUuid>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QRegExp>
+#include <QStringList>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include <QPainter>
+#include <minecraft/auth/flows/MSASilent.h>
+#include <minecraft/auth/flows/MSAInteractive.h>
+
+#include <minecraft/auth/flows/MojangRefresh.h>
+#include <minecraft/auth/flows/MojangLogin.h>
+
+MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
+ MinecraftAccountPtr account(new MinecraftAccount());
+ if(account->data.resumeStateFromV2(json)) {
+ return account;
+ }
+ return nullptr;
+}
+
+MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
+ MinecraftAccountPtr account(new MinecraftAccount());
+ if(account->data.resumeStateFromV3(json)) {
+ return account;
+ }
+ return nullptr;
+}
+
+MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
+{
+ MinecraftAccountPtr account(new MinecraftAccount());
+ account->data.type = AccountType::Mojang;
+ account->data.yggdrasilToken.extra["userName"] = username;
+ account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
+ return account;
+}
+
+MinecraftAccountPtr MinecraftAccount::createBlankMSA()
+{
+ MinecraftAccountPtr account(new MinecraftAccount());
+ account->data.type = AccountType::MSA;
+ return account;
+}
+
+
+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;
+ }
+}
+
+QPixmap MinecraftAccount::getFace() const {
+ QPixmap skinTexture;
+ if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
+ return QPixmap();
+ }
+ QPixmap skin = QPixmap(8, 8);
+ QPainter painter(&skin);
+ painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
+ painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
+ return skin.scaled(64, 64, Qt::KeepAspectRatio);
+}
+
+
+std::shared_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, 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)));
+ }
+ return m_currentTask;
+}
+
+std::shared_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) {
+ 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)));
+ }
+ return m_currentTask;
+}
+
+std::shared_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;
+ }
+
+ if(accountStatus() == Verified && !session->wants_online)
+ {
+ session->status = AuthSession::PlayableOffline;
+ session->auth_server_online = false;
+ fillSession(session);
+ return nullptr;
+ }
+ 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)));
+ }
+ return m_currentTask;
+}
+
+
+void MinecraftAccount::authSucceeded()
+{
+ auto session = m_currentTask->getAssignedSession();
+ if (session)
+ {
+ session->status =
+ session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
+ fillSession(session);
+ session->auth_server_online = true;
+ }
+ m_currentTask.reset();
+ emit changed();
+}
+
+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
+ if (m_currentTask->accountState() == AccountTask::STATE_FAILED_SOFT)
+ {
+ if (session)
+ {
+ session->status = accountStatus() == Verified ? AuthSession::PlayableOffline : AuthSession::RequiresPassword;
+ session->auth_server_online = false;
+ fillSession(session);
+ }
+ }
+ else
+ {
+ // FIXME: MSA ...
+ data.yggdrasilToken.token = QString();
+ data.yggdrasilToken.validity = Katabasis::Validity::None;
+ data.validity_ = Katabasis::Validity::None;
+ emit changed();
+ if (session)
+ {
+ session->status = AuthSession::RequiresPassword;
+ session->auth_server_online = true;
+ fillSession(session);
+ }
+ }
+ m_currentTask.reset();
+}
+
+void MinecraftAccount::fillSession(AuthSessionPtr session)
+{
+ // the user name. you have to have an user name
+ // FIXME: not with MSA
+ session->username = data.userName();
+ // volatile auth token
+ session->access_token = data.accessToken();
+ // the semi-permanent client token
+ session->client_token = data.clientToken();
+ // profile name
+ session->player_name = data.profileName();
+ // profile ID
+ session->uuid = data.profileId();
+ // 'legacy' or 'mojang', depending on account type
+ session->user_type = typeString();
+ if (!session->access_token.isEmpty())
+ {
+ session->session = "token:" + data.accessToken() + ":" + data.profileId();
+ }
+ else
+ {
+ session->session = "-";
+ }
+ session->m_accountPtr = shared_from_this();
+}
+
+void MinecraftAccount::decrementUses()
+{
+ Usable::decrementUses();
+ if(!isInUse())
+ {
+ emit changed();
+ // FIXME: we now need a better way to identify accounts...
+ qWarning() << "Profile" << data.profileId() << "is no longer in use.";
+ }
+}
+
+void MinecraftAccount::incrementUses()
+{
+ bool wasInUse = isInUse();
+ Usable::incrementUses();
+ if(!wasInUse)
+ {
+ emit changed();
+ // FIXME: we now need a better way to identify accounts...
+ qWarning() << "Profile" << data.profileId() << "is now in use.";
+ }
+}
diff --git a/launcher/minecraft/auth/MojangAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index 3f6cbedd..72bb6bd4 100644
--- a/launcher/minecraft/auth/MojangAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -21,17 +21,19 @@
#include <QJsonObject>
#include <QPair>
#include <QMap>
+#include <QPixmap>
#include <memory>
#include "AuthSession.h"
#include "Usable.h"
+#include "AccountData.h"
class Task;
-class YggdrasilTask;
-class MojangAccount;
+class AccountTask;
+class MinecraftAccount;
-typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
-Q_DECLARE_METATYPE(MojangAccountPtr)
+typedef std::shared_ptr<MinecraftAccount> MinecraftAccountPtr;
+Q_DECLARE_METATYPE(MinecraftAccountPtr)
/**
* A profile within someone's Mojang account.
@@ -59,75 +61,90 @@ enum AccountStatus
* Said information may include things such as that account's username, client token, and access
* token if the user chose to stay logged in.
*/
-class MojangAccount :
+class MinecraftAccount :
public QObject,
public Usable,
- public std::enable_shared_from_this<MojangAccount>
+ public std::enable_shared_from_this<MinecraftAccount>
{
Q_OBJECT
public: /* construction */
//! Do not copy accounts. ever.
- explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;
+ explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete;
//! Default constructor
- explicit MojangAccount(QObject *parent = 0) : QObject(parent) {};
+ explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {};
- //! Creates an empty account for the specified user name.
- static MojangAccountPtr createFromUsername(const QString &username);
+ static MinecraftAccountPtr createFromUsername(const QString &username);
- //! Loads a MojangAccount from the given JSON object.
- static MojangAccountPtr loadFromJson(const QJsonObject &json);
+ static MinecraftAccountPtr createBlankMSA();
- //! Saves a MojangAccount to a JSON object and returns it.
+ static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
+ static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
+
+ //! Saves a MinecraftAccount to a JSON object and returns it.
QJsonObject saveToJson() const;
public: /* manipulation */
- /**
- * Sets the currently selected profile to the profile with the given ID string.
- * If profileId is not in the list of available profiles, the function will simply return
- * false.
- */
- bool setCurrentProfile(const QString &profileId);
/**
* Attempt to login. Empty password means we use the token.
* If the attempt fails because we already are performing some task, it returns false.
*/
- std::shared_ptr<YggdrasilTask> login(AuthSessionPtr session, QString password = QString());
- void invalidateClientToken();
+ std::shared_ptr<AccountTask> login(AuthSessionPtr session, QString password = QString());
+
+ std::shared_ptr<AccountTask> loginMSA(AuthSessionPtr session);
+
+ std::shared_ptr<AccountTask> refresh(AuthSessionPtr session);
public: /* queries */
- const QString &username() const
- {
- return m_username;
+ QString accountDisplayString() const {
+ return data.accountDisplayString();
}
- const QString &clientToken() const
- {
- return m_clientToken;
+ QString mojangUserName() const {
+ return data.userName();
}
- const QString &accessToken() const
- {
- return m_accessToken;
+ QString accessToken() const {
+ return data.accessToken();
}
- const QList<AccountProfile> &profiles() const
- {
- return m_profiles;
+ QString profileId() const {
+ return data.profileId();
}
- const User &user()
- {
- return m_user;
+ QString profileName() const {
+ return data.profileName();
}
- //! Returns the currently selected profile (if none, returns nullptr)
- const AccountProfile *currentProfile() const;
+ QString typeString() const {
+ switch(data.type) {
+ case AccountType::Mojang: {
+ if(data.legacy) {
+ return "legacy";
+ }
+ return "mojang";
+ }
+ break;
+ case AccountType::MSA: {
+ return "msa";
+ }
+ break;
+ default: {
+ return "unknown";
+ }
+ }
+ }
+
+ QPixmap getFace() const;
//! Returns whether the account is NotVerified, Verified or Online
AccountStatus accountStatus() const;
+ AccountData * accountData() {
+ return &data;
+ }
+
signals:
/**
* This signal is emitted when the account changes
@@ -137,27 +154,10 @@ signals:
// TODO: better signalling for the various possible state changes - especially errors
protected: /* variables */
- QString m_username;
-
- // Used to identify the client - the user can have multiple clients for the same account
- // Think: different launchers, all connecting to the same account/profile
- QString m_clientToken;
-
- // Blank if not logged in.
- QString m_accessToken;
-
- // Index of the selected profile within the list of available
- // profiles. -1 if nothing is selected.
- int m_currentProfile = -1;
-
- // List of available profiles.
- QList<AccountProfile> m_profiles;
-
- // the user structure, whatever it is.
- User m_user;
+ AccountData data;
// current task we are executing here
- std::shared_ptr<YggdrasilTask> m_currentTask;
+ std::shared_ptr<AccountTask> m_currentTask;
protected: /* methods */
@@ -171,10 +171,4 @@ slots:
private:
void fillSession(AuthSessionPtr session);
-
-public:
- friend class YggdrasilTask;
- friend class AuthenticateTask;
- friend class ValidateTask;
- friend class RefreshTask;
};
diff --git a/launcher/minecraft/auth/MojangAccount.cpp b/launcher/minecraft/auth/MojangAccount.cpp
deleted file mode 100644
index f5853fe3..00000000
--- a/launcher/minecraft/auth/MojangAccount.cpp
+++ /dev/null
@@ -1,315 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "MojangAccount.h"
-#include "flows/RefreshTask.h"
-#include "flows/AuthenticateTask.h"
-
-#include <QUuid>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QRegExp>
-#include <QStringList>
-#include <QJsonDocument>
-
-#include <QDebug>
-
-MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
-{
- // The JSON object must at least have a username for it to be valid.
- if (!object.value("username").isString())
- {
- qCritical() << "Can't load Mojang account info from JSON object. Username field is "
- "missing or of the wrong type.";
- return nullptr;
- }
-
- QString username = object.value("username").toString("");
- QString clientToken = object.value("clientToken").toString("");
- QString accessToken = object.value("accessToken").toString("");
-
- QJsonArray profileArray = object.value("profiles").toArray();
- if (profileArray.size() < 1)
- {
- qCritical() << "Can't load Mojang account with username \"" << username
- << "\". No profiles found.";
- return nullptr;
- }
-
- QList<AccountProfile> profiles;
- for (QJsonValue profileVal : profileArray)
- {
- QJsonObject profileObject = profileVal.toObject();
- QString id = profileObject.value("id").toString("");
- QString name = profileObject.value("name").toString("");
- bool legacy = profileObject.value("legacy").toBool(false);
- if (id.isEmpty() || name.isEmpty())
- {
- qWarning() << "Unable to load a profile because it was missing an ID or a name.";
- continue;
- }
- profiles.append({id, name, legacy});
- }
-
- MojangAccountPtr account(new MojangAccount());
- if (object.value("user").isObject())
- {
- User u;
- QJsonObject userStructure = object.value("user").toObject();
- u.id = userStructure.value("id").toString();
- /*
- QJsonObject propMap = userStructure.value("properties").toObject();
- for(auto key: propMap.keys())
- {
- auto values = propMap.operator[](key).toArray();
- for(auto value: values)
- u.properties.insert(key, value.toString());
- }
- */
- account->m_user = u;
- }
- account->m_username = username;
- account->m_clientToken = clientToken;
- account->m_accessToken = accessToken;
- account->m_profiles = profiles;
-
- // Get the currently selected profile.
- QString currentProfile = object.value("activeProfile").toString("");
- if (!currentProfile.isEmpty())
- account->setCurrentProfile(currentProfile);
-
- return account;
-}
-
-MojangAccountPtr MojangAccount::createFromUsername(const QString &username)
-{
- MojangAccountPtr account(new MojangAccount());
- account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
- account->m_username = username;
- return account;
-}
-
-QJsonObject MojangAccount::saveToJson() const
-{
- QJsonObject json;
- json.insert("username", m_username);
- json.insert("clientToken", m_clientToken);
- json.insert("accessToken", m_accessToken);
-
- QJsonArray profileArray;
- for (AccountProfile profile : m_profiles)
- {
- QJsonObject profileObj;
- profileObj.insert("id", profile.id);
- profileObj.insert("name", profile.name);
- profileObj.insert("legacy", profile.legacy);
- profileArray.append(profileObj);
- }
- json.insert("profiles", profileArray);
-
- QJsonObject userStructure;
- {
- userStructure.insert("id", m_user.id);
- /*
- QJsonObject userAttrs;
- for(auto key: m_user.properties.keys())
- {
- auto array = QJsonArray::fromStringList(m_user.properties.values(key));
- userAttrs.insert(key, array);
- }
- userStructure.insert("properties", userAttrs);
- */
- }
- json.insert("user", userStructure);
-
- if (m_currentProfile != -1)
- json.insert("activeProfile", currentProfile()->id);
-
- return json;
-}
-
-bool MojangAccount::setCurrentProfile(const QString &profileId)
-{
- for (int i = 0; i < m_profiles.length(); i++)
- {
- if (m_profiles[i].id == profileId)
- {
- m_currentProfile = i;
- return true;
- }
- }
- return false;
-}
-
-const AccountProfile *MojangAccount::currentProfile() const
-{
- if (m_currentProfile == -1)
- return nullptr;
- return &m_profiles[m_currentProfile];
-}
-
-AccountStatus MojangAccount::accountStatus() const
-{
- if (m_accessToken.isEmpty())
- return NotVerified;
- else
- return Verified;
-}
-
-std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session, 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 RefreshTask(this));
- }
- else
- {
- m_currentTask.reset(new AuthenticateTask(this, password));
- }
- m_currentTask->assignSession(session);
-
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
- }
- return m_currentTask;
-}
-
-void MojangAccount::authSucceeded()
-{
- auto session = m_currentTask->getAssignedSession();
- if (session)
- {
- session->status =
- session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
- fillSession(session);
- session->auth_server_online = true;
- }
- m_currentTask.reset();
- emit changed();
-}
-
-void MojangAccount::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
- if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
- {
- if (session)
- {
- session->status = accountStatus() == Verified ? AuthSession::PlayableOffline
- : AuthSession::RequiresPassword;
- session->auth_server_online = false;
- fillSession(session);
- }
- }
- else
- {
- m_accessToken = QString();
- emit changed();
- if (session)
- {
- session->status = AuthSession::RequiresPassword;
- session->auth_server_online = true;
- fillSession(session);
- }
- }
- m_currentTask.reset();
-}
-
-void MojangAccount::fillSession(AuthSessionPtr session)
-{
- // the user name. you have to have an user name
- session->username = m_username;
- // volatile auth token
- session->access_token = m_accessToken;
- // the semi-permanent client token
- session->client_token = m_clientToken;
- if (currentProfile())
- {
- // profile name
- session->player_name = currentProfile()->name;
- // profile ID
- session->uuid = currentProfile()->id;
- // 'legacy' or 'mojang', depending on account type
- session->user_type = currentProfile()->legacy ? "legacy" : "mojang";
- if (!session->access_token.isEmpty())
- {
- session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id;
- }
- else
- {
- session->session = "-";
- }
- }
- else
- {
- session->player_name = "Player";
- session->session = "-";
- }
- session->u = user();
- session->m_accountPtr = shared_from_this();
-}
-
-void MojangAccount::decrementUses()
-{
- Usable::decrementUses();
- if(!isInUse())
- {
- emit changed();
- qWarning() << "Account" << m_username << "is no longer in use.";
- }
-}
-
-void MojangAccount::incrementUses()
-{
- bool wasInUse = isInUse();
- Usable::incrementUses();
- if(!wasInUse)
- {
- emit changed();
- qWarning() << "Account" << m_username << "is now in use.";
- }
-}
-
-void MojangAccount::invalidateClientToken()
-{
- m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
- emit changed();
-}
diff --git a/launcher/minecraft/auth/MojangAccountList.h b/launcher/minecraft/auth/MojangAccountList.h
deleted file mode 100644
index 99d2988e..00000000
--- a/launcher/minecraft/auth/MojangAccountList.h
+++ /dev/null
@@ -1,199 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "MojangAccount.h"
-
-#include <QObject>
-#include <QVariant>
-#include <QAbstractListModel>
-#include <QSharedPointer>
-
-/*!
- * \brief List of available Mojang accounts.
- * This should be loaded in the background by MultiMC on startup.
- *
- * This class also inherits from QAbstractListModel. Methods from that
- * class determine how this list shows up in a list view. Said methods
- * all have a default implementation, but they can be overridden by subclasses to
- * change the behavior of the list.
- */
-class MojangAccountList : public QAbstractListModel
-{
- Q_OBJECT
-public:
- enum ModelRoles
- {
- PointerRole = 0x34B1CB48
- };
-
- enum VListColumns
- {
- // TODO: Add icon column.
-
- // First column - Active?
- ActiveColumn = 0,
-
- // Second column - Name
- NameColumn,
- };
-
- explicit MojangAccountList(QObject *parent = 0);
-
- //! Gets the account at the given index.
- virtual const MojangAccountPtr at(int i) const;
-
- //! Returns the number of accounts in the list.
- virtual int count() const;
-
- //////// List Model Functions ////////
- virtual QVariant data(const QModelIndex &index, int role) const;
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
- virtual int rowCount(const QModelIndex &parent) const;
- virtual int columnCount(const QModelIndex &parent) const;
- virtual Qt::ItemFlags flags(const QModelIndex &index) const;
- virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
-
- /*!
- * Adds a the given Mojang account to the account list.
- */
- virtual void addAccount(const MojangAccountPtr account);
-
- /*!
- * Removes the mojang account with the given username from the account list.
- */
- virtual void removeAccount(const QString &username);
-
- /*!
- * Removes the account at the given QModelIndex.
- */
- virtual void removeAccount(QModelIndex index);
-
- /*!
- * \brief Finds an account by its username.
- * \param The username of the account to find.
- * \return A const pointer to the account with the given username. NULL if
- * one doesn't exist.
- */
- virtual MojangAccountPtr findAccount(const QString &username) const;
-
- /*!
- * Sets the default path to save the list file to.
- * If autosave is true, this list will automatically save to the given path whenever it changes.
- * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
- * after calling this function to ensure an autosaved change doesn't overwrite the list you intended
- * to load.
- */
- virtual void setListFilePath(QString path, bool autosave = false);
-
- /*!
- * \brief Loads the account list from the given file path.
- * If the given file is an empty string (default), will load from the default account list file.
- * \return True if successful, otherwise false.
- */
- virtual bool loadList(const QString &file = "");
-
- /*!
- * \brief Saves the account list to the given file.
- * If the given file is an empty string (default), will save from the default account list file.
- * \return True if successful, otherwise false.
- */
- virtual bool saveList(const QString &file = "");
-
- /*!
- * \brief Gets a pointer to the account that the user has selected as their "active" account.
- * Which account is active can be overridden on a per-instance basis, but this will return the one that
- * is set as active globally.
- * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer.
- */
- virtual MojangAccountPtr activeAccount() const;
-
- /*!
- * Sets the given account as the current active account.
- * If the username given is an empty string, sets the active account to nothing.
- */
- virtual void setActiveAccount(const QString &username);
-
- /*!
- * Returns true if any of the account is at least Validated
- */
- bool anyAccountIsValid();
-
-signals:
- /*!
- * Signal emitted to indicate that the account list has changed.
- * This will also fire if the value of an element in the list changes (will be implemented
- * later).
- */
- void listChanged();
-
- /*!
- * Signal emitted to indicate that the active account has changed.
- */
- void activeAccountChanged();
-
-public
-slots:
- /**
- * This is called when one of the accounts changes and the list needs to be updated
- */
- void accountChanged();
-
-protected:
- /*!
- * Called whenever the list changes.
- * This emits the listChanged() signal and autosaves the list (if autosave is enabled).
- */
- void onListChanged();
-
- /*!
- * Called whenever the active account changes.
- * Emits the activeAccountChanged() signal and autosaves the list if enabled.
- */
- void onActiveChanged();
-
- QList<MojangAccountPtr> m_accounts;
-
- /*!
- * Account that is currently active.
- */
- MojangAccountPtr m_activeAccount;
-
- //! Path to the account list file. Empty string if there isn't one.
- QString m_listFilePath;
-
- /*!
- * If true, the account list will automatically save to the account list path when it changes.
- * Ignored if m_listFilePath is blank.
- */
- bool m_autosave = false;
-
-protected
-slots:
- /*!
- * Updates this list with the given list of accounts.
- * This is done by copying each account in the given list and inserting it
- * into this one.
- * We need to do this so that we can set the parents of the accounts are set to this
- * account list. This can't be done in the load task, because the accounts the load
- * task creates are on the load task's thread and Qt won't allow their parents
- * to be set to something created on another thread.
- * To get around that problem, we invoke this method on the GUI thread, which
- * then copies the accounts and sets their parents correctly.
- * \param accounts List of accounts whose parents should be set.
- */
- virtual void updateListData(QList<MojangAccountPtr> versions);
-};
diff --git a/launcher/minecraft/auth-msa/context.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp
index d7ecda30..9aa58ac3 100644
--- a/launcher/minecraft/auth-msa/context.cpp
+++ b/launcher/minecraft/auth/flows/AuthContext.cpp
@@ -14,9 +14,8 @@
#include <QPixmap>
#include <QPainter>
-#include "context.h"
+#include "AuthContext.h"
#include "katabasis/Globals.h"
-#include "katabasis/StoreQSettings.h"
#include "katabasis/Requestor.h"
#include "BuildConfig.h"
@@ -24,117 +23,107 @@ using OAuth2 = Katabasis::OAuth2;
using Requestor = Katabasis::Requestor;
using Activity = Katabasis::Activity;
-Context::Context(QObject *parent) :
- QObject(parent)
+AuthContext::AuthContext(AccountData * data, QObject *parent) :
+ AccountTask(data, parent)
{
mgr = new QNetworkAccessManager(this);
-
- Katabasis::OAuth2::Options opts;
- opts.scope = "XboxLive.signin offline_access";
- opts.clientIdentifier = BuildConfig.CLIENT_ID;
- opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
- opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
- opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
-
- oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr);
-
- connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed);
- connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded);
- connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser);
- connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser);
- connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged);
}
-void Context::beginActivity(Activity activity) {
+void AuthContext::beginActivity(Activity activity) {
if(isBusy()) {
throw 0;
}
- activity_ = activity;
- emit activityChanged(activity_);
+ m_activity = activity;
+ changeState(STATE_WORKING, "Initializing");
+ emit activityChanged(m_activity);
}
-void Context::finishActivity() {
+void AuthContext::finishActivity() {
if(!isBusy()) {
throw 0;
}
- activity_ = Katabasis::Activity::Idle;
- m_account.validity_ = m_account.minecraftProfile.validity;
- emit activityChanged(activity_);
+ m_activity = Katabasis::Activity::Idle;
+ m_stage = MSAStage::Idle;
+ m_data->validity_ = m_data->minecraftProfile.validity;
+ emit activityChanged(m_activity);
}
-QString Context::gameToken() {
- return m_account.minecraftToken.token;
-}
+void AuthContext::initMSA() {
+ if(m_oauth2) {
+ return;
+ }
+ Katabasis::OAuth2::Options opts;
+ opts.scope = "XboxLive.signin offline_access";
+ opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID;
+ opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
+ opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
+ opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
-QString Context::userId() {
- return m_account.minecraftProfile.id;
-}
+ m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr);
-QString Context::userName() {
- return m_account.minecraftProfile.name;
+ connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
+ connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
+ connect(m_oauth2, &OAuth2::openBrowser, this, &AuthContext::onOpenBrowser);
+ connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser);
+ connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
}
-bool Context::silentSignIn() {
- if(isBusy()) {
- return false;
- }
- beginActivity(Activity::Refreshing);
- if(!oauth2->refresh()) {
- finishActivity();
- return false;
+void AuthContext::initMojang() {
+ if(m_yggdrasil) {
+ return;
}
+ m_yggdrasil = new Yggdrasil(m_data, this);
- requestsDone = 0;
- xboxProfileSucceeded = false;
- mcAuthSucceeded = false;
-
- return true;
+ connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
+ connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
}
-bool Context::signIn() {
- if(isBusy()) {
- return false;
- }
+void AuthContext::onMojangSucceeded() {
+ doMinecraftProfile();
+}
- requestsDone = 0;
- xboxProfileSucceeded = false;
- mcAuthSucceeded = false;
- beginActivity(Activity::LoggingIn);
- oauth2->unlink();
- m_account = AccountData();
- oauth2->link();
- return true;
+void AuthContext::onMojangFailed() {
+ finishActivity();
+ m_error = m_yggdrasil->m_error;
+ m_aborted = m_yggdrasil->m_aborted;
+ changeState(m_yggdrasil->accountState(), "Microsoft user authentication failed.");
}
-bool Context::signOut() {
+/*
+bool AuthContext::signOut() {
if(isBusy()) {
return false;
}
+
+ start();
+
beginActivity(Activity::LoggingOut);
- oauth2->unlink();
+ m_oauth2->unlink();
m_account = AccountData();
finishActivity();
return true;
}
+*/
-
-void Context::onOpenBrowser(const QUrl &url) {
+void AuthContext::onOpenBrowser(const QUrl &url) {
QDesktopServices::openUrl(url);
}
-void Context::onCloseBrowser() {
+void AuthContext::onCloseBrowser() {
}
-void Context::onLinkingFailed() {
+void AuthContext::onOAuthLinkingFailed() {
finishActivity();
+ changeState(STATE_FAILED_HARD, "Microsoft user authentication failed.");
}
-void Context::onLinkingSucceeded() {
+void AuthContext::onOAuthLinkingSucceeded() {
auto *o2t = qobject_cast<OAuth2 *>(sender());
if (!o2t->linked()) {
finishActivity();
+ changeState(STATE_FAILED_HARD, "Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).");
return;
}
QVariantMap extraTokens = o2t->extraTokens();
@@ -147,11 +136,14 @@ void Context::onLinkingSucceeded() {
doUserAuth();
}
-void Context::onOAuthActivityChanged(Katabasis::Activity activity) {
+void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
// respond to activity change here
}
-void Context::doUserAuth() {
+void AuthContext::doUserAuth() {
+ m_stage = MSAStage::UserAuth;
+ changeState(STATE_WORKING, "Starting user authentication");
+
QString xbox_auth_template = R"XXX(
{
"Properties": {
@@ -163,15 +155,15 @@ void Context::doUserAuth() {
"TokenType": "JWT"
}
)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_account.msaToken.token);
+ 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 Katabasis::Requestor(mgr, oauth2, this);
+ auto *requestor = new Katabasis::Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "First layer of XBox auth ... commencing.";
}
@@ -181,7 +173,7 @@ bool getDateTime(QJsonValue value, QDateTime & out) {
if(!value.isString()) {
return false;
}
- out = QDateTime::fromString(value.toString(), Qt::ISODateWithMs);
+ out = QDateTime::fromString(value.toString(), Qt::ISODate);
return out.isValid();
}
@@ -294,7 +286,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) {
}
-void Context::onUserAuthDone(
+void AuthContext::onUserAuthDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
@@ -303,6 +295,7 @@ void Context::onUserAuthDone(
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
finishActivity();
+ changeState(STATE_FAILED_HARD, "XBox user authentication failed.");
return;
}
@@ -310,9 +303,13 @@ void Context::onUserAuthDone(
if(!parseXTokenResponse(replyData, temp)) {
qWarning() << "Could not parse user authentication response...";
finishActivity();
+ changeState(STATE_FAILED_HARD, "XBox user authentication response could not be understood.");
return;
}
- m_account.userToken = temp;
+ m_data->userToken = temp;
+
+ m_stage = MSAStage::XboxAuth;
+ changeState(STATE_WORKING, "Starting XBox authentication");
doSTSAuthMinecraft();
doSTSAuthGeneric();
@@ -329,7 +326,7 @@ void Context::onUserAuthDone(
},
}
*/
-void Context::doSTSAuthMinecraft() {
+void AuthContext::doSTSAuthMinecraft() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
@@ -342,20 +339,20 @@ void Context::doSTSAuthMinecraft() {
"TokenType": "JWT"
}
)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
+ 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");
- Requestor *requestor = new Requestor(mgr, oauth2, this);
+ Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Second layer of XBox auth ... commencing.";
}
-void Context::onSTSAuthMinecraftDone(
+void AuthContext::onSTSAuthMinecraftDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
@@ -363,29 +360,29 @@ void Context::onSTSAuthMinecraftDone(
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
- finishActivity();
+ m_requestsDone ++;
return;
}
Katabasis::Token temp;
if(!parseXTokenResponse(replyData, temp)) {
qWarning() << "Could not parse authorization response for access to mojang services...";
- finishActivity();
+ m_requestsDone ++;
return;
}
- if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
+ if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
qDebug() << replyData;
- finishActivity();
+ m_requestsDone ++;
return;
}
- m_account.mojangservicesToken = temp;
+ m_data->mojangservicesToken = temp;
doMinecraftAuth();
}
-void Context::doSTSAuthGeneric() {
+void AuthContext::doSTSAuthGeneric() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
@@ -398,20 +395,20 @@ void Context::doSTSAuthGeneric() {
"TokenType": "JWT"
}
)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
+ 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");
- Requestor *requestor = new Requestor(mgr, oauth2, this);
+ Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Second layer of XBox auth ... commencing.";
}
-void Context::onSTSAuthGenericDone(
+void AuthContext::onSTSAuthGenericDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
@@ -419,44 +416,44 @@ void Context::onSTSAuthGenericDone(
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
- finishActivity();
+ m_requestsDone ++;
return;
}
Katabasis::Token temp;
if(!parseXTokenResponse(replyData, temp)) {
qWarning() << "Could not parse authorization response for access to xbox API...";
- finishActivity();
+ m_requestsDone ++;
return;
}
- if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
+ if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
qDebug() << replyData;
- finishActivity();
+ m_requestsDone ++;
return;
}
- m_account.xboxApiToken = temp;
+ m_data->xboxApiToken = temp;
doXBoxProfile();
}
-void Context::doMinecraftAuth() {
+void AuthContext::doMinecraftAuth() {
QString mc_auth_template = R"XXX(
{
"identityToken": "XBL3.0 x=%1;%2"
}
)XXX";
- auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.mojangservicesToken.token);
+ auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->mojangservicesToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
- Requestor *requestor = new Requestor(mgr, oauth2, this);
+ Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone);
requestor->post(request, data.toUtf8());
qDebug() << "Getting Minecraft access token...";
}
@@ -501,33 +498,31 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
}
}
-void Context::onMinecraftAuthDone(
+void AuthContext::onMinecraftAuthDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
- requestsDone++;
+ m_requestsDone ++;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qDebug() << replyData;
- finishActivity();
return;
}
- if(!parseMojangResponse(replyData, m_account.minecraftToken)) {
+ if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response...";
qDebug() << replyData;
- finishActivity();
return;
}
- mcAuthSucceeded = true;
+ m_mcAuthSucceeded = true;
checkResult();
}
-void Context::doXBoxProfile() {
+void AuthContext::doXBoxProfile() {
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
QUrlQuery q;
q.addQueryItem(
@@ -544,45 +539,45 @@ void Context::doXBoxProfile() {
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_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8());
- Requestor *requestor = new Requestor(mgr, oauth2, this);
+ request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
+ Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone);
requestor->get(request);
qDebug() << "Getting Xbox profile...";
}
-void Context::onXBoxProfileDone(
+void AuthContext::onXBoxProfileDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
- requestsDone ++;
+ m_requestsDone ++;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qDebug() << replyData;
- finishActivity();
return;
}
qDebug() << "XBox profile: " << replyData;
- xboxProfileSucceeded = true;
+ m_xboxProfileSucceeded = true;
checkResult();
}
-void Context::checkResult() {
- if(requestsDone != 2) {
+void AuthContext::checkResult() {
+ if(m_requestsDone != 2) {
return;
}
- if(mcAuthSucceeded && xboxProfileSucceeded) {
+ if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
doMinecraftProfile();
}
else {
finishActivity();
+ changeState(STATE_FAILED_HARD, "XBox and/or Mojang authentication steps did not succeed");
}
}
@@ -666,273 +661,92 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
}
}
-void Context::doMinecraftProfile() {
+void AuthContext::doMinecraftProfile() {
+ m_stage = MSAStage::MinecraftProfile;
+ changeState(STATE_WORKING, "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_account.minecraftToken.token).toUtf8());
+ request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
- Requestor *requestor = new Requestor(mgr, oauth2, this);
+ Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone);
requestor->get(request);
}
-void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
+void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
qDebug() << data;
if (error == QNetworkReply::ContentNotFoundError) {
- m_account.minecraftProfile = MinecraftProfile();
+ m_data->minecraftProfile = MinecraftProfile();
finishActivity();
+ changeState(STATE_FAILED_HARD, "Account is missing a profile");
return;
}
if (error != QNetworkReply::NoError) {
finishActivity();
+ changeState(STATE_FAILED_HARD, "Profile acquisition failed");
return;
}
- if(!parseMinecraftProfile(data, m_account.minecraftProfile)) {
- m_account.minecraftProfile = MinecraftProfile();
+ if(!parseMinecraftProfile(data, m_data->minecraftProfile)) {
+ m_data->minecraftProfile = MinecraftProfile();
finishActivity();
+ changeState(STATE_FAILED_HARD, "Profile response could not be parsed");
return;
}
doGetSkin();
}
-void Context::doGetSkin() {
- auto url = QUrl(m_account.minecraftProfile.skin.url);
+void AuthContext::doGetSkin() {
+ m_stage = MSAStage::Skin;
+ changeState(STATE_WORKING, "Starting skin acquisition");
+
+ auto url = QUrl(m_data->minecraftProfile.skin.url);
QNetworkRequest request = QNetworkRequest(url);
- Requestor *requestor = new Requestor(mgr, oauth2, this);
+ Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
- connect(requestor, &Requestor::finished, this, &Context::onSkinDone);
+ connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone);
requestor->get(request);
}
-void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
+void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
if (error == QNetworkReply::NoError) {
- m_account.minecraftProfile.skin.data = data;
+ m_data->minecraftProfile.skin.data = data;
}
+ m_data->validity_ = Katabasis::Validity::Certain;
finishActivity();
+ changeState(STATE_SUCCEEDED, "Finished whole chain");
}
-namespace {
-void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
- if(t.validity == Katabasis::Validity::None || !t.persistent) {
- return;
- }
- QJsonObject out;
- if(t.issueInstant.isValid()) {
- out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch());
- }
-
- if(t.notAfter.isValid()) {
- out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch());
- }
-
- if(!t.token.isEmpty()) {
- out["token"] = QJsonValue(t.token);
- }
- if(!t.refresh_token.isEmpty()) {
- out["refresh_token"] = QJsonValue(t.refresh_token);
- }
- if(t.extra.size()) {
- out["extra"] = QJsonObject::fromVariantMap(t.extra);
- }
- if(out.size()) {
- parent[tokenName] = out;
- }
-}
-
-Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) {
- Katabasis::Token out;
- auto tokenObject = parent.value(tokenName).toObject();
- if(tokenObject.isEmpty()) {
- return out;
- }
- auto issueInstant = tokenObject.value("iat");
- if(issueInstant.isDouble()) {
- out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble());
- }
-
- auto notAfter = tokenObject.value("exp");
- if(notAfter.isDouble()) {
- out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble());
- }
-
- auto token = tokenObject.value("token");
- if(token.isString()) {
- out.token = token.toString();
- out.validity = Katabasis::Validity::Assumed;
- }
-
- auto refresh_token = tokenObject.value("refresh_token");
- if(refresh_token.isString()) {
- out.refresh_token = refresh_token.toString();
- }
-
- auto extra = tokenObject.value("extra");
- if(extra.isObject()) {
- out.extra = extra.toObject().toVariantMap();
- }
- return out;
-}
-
-void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
- if(p.id.isEmpty()) {
- return;
- }
- QJsonObject out;
- out["id"] = QJsonValue(p.id);
- out["name"] = QJsonValue(p.name);
- if(p.currentCape != -1) {
- out["cape"] = p.capes[p.currentCape].id;
- }
-
- {
- QJsonObject skinObj;
- skinObj["id"] = p.skin.id;
- skinObj["url"] = p.skin.url;
- skinObj["variant"] = p.skin.variant;
- if(p.skin.data.size()) {
- skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
- }
- out["skin"] = skinObj;
- }
-
- QJsonArray capesArray;
- for(auto & cape: p.capes) {
- QJsonObject capeObj;
- capeObj["id"] = cape.id;
- capeObj["url"] = cape.url;
- capeObj["alias"] = cape.alias;
- if(cape.data.size()) {
- capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
- }
- capesArray.push_back(capeObj);
- }
- out["capes"] = capesArray;
- parent[tokenName] = out;
-}
-
-MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) {
- MinecraftProfile out;
- auto tokenObject = parent.value(tokenName).toObject();
- if(tokenObject.isEmpty()) {
- return out;
- }
- {
- auto idV = tokenObject.value("id");
- auto nameV = tokenObject.value("name");
- if(!idV.isString() || !nameV.isString()) {
- qWarning() << "mandatory profile attributes are missing or of unexpected type";
- return MinecraftProfile();
- }
- out.name = nameV.toString();
- out.id = idV.toString();
- }
-
+QString AuthContext::getStateMessage() const {
+ switch (m_accountState)
{
- auto skinV = tokenObject.value("skin");
- if(!skinV.isObject()) {
- qWarning() << "skin is missing";
- return MinecraftProfile();
- }
- auto skinObj = skinV.toObject();
- auto idV = skinObj.value("id");
- auto urlV = skinObj.value("url");
- auto variantV = skinObj.value("variant");
- if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
- qWarning() << "mandatory skin attributes are missing or of unexpected type";
- return MinecraftProfile();
- }
- out.skin.id = idV.toString();
- out.skin.url = urlV.toString();
- out.skin.variant = variantV.toString();
-
- // data for skin is optional
- auto dataV = skinObj.value("data");
- if(dataV.isString()) {
- // TODO: validate base64
- out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
- }
- else if (!dataV.isUndefined()) {
- qWarning() << "skin data is something unexpected";
- return MinecraftProfile();
- }
- }
-
- auto capesV = tokenObject.value("capes");
- if(!capesV.isArray()) {
- qWarning() << "capes is not an array!";
- return MinecraftProfile();
- }
- auto capesArray = capesV.toArray();
- for(auto capeV: capesArray) {
- if(!capeV.isObject()) {
- qWarning() << "cape is not an object!";
- return MinecraftProfile();
- }
- auto capeObj = capeV.toObject();
- auto idV = capeObj.value("id");
- auto urlV = capeObj.value("url");
- auto aliasV = capeObj.value("alias");
- if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
- qWarning() << "mandatory skin attributes are missing or of unexpected type";
- return MinecraftProfile();
- }
- Cape cape;
- cape.id = idV.toString();
- cape.url = urlV.toString();
- cape.alias = aliasV.toString();
-
- // data for cape is optional.
- auto dataV = capeObj.value("data");
- if(dataV.isString()) {
- // TODO: validate base64
- cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
- }
- else if (!dataV.isUndefined()) {
- qWarning() << "cape data is something unexpected";
- return MinecraftProfile();
- }
- out.capes.push_back(cape);
- }
- out.validity = Katabasis::Validity::Assumed;
- return out;
-}
-
-}
-
-bool Context::resumeFromState(QByteArray data) {
- QJsonParseError error;
- auto doc = QJsonDocument::fromJson(data, &error);
- if(error.error != QJsonParseError::NoError) {
- qWarning() << "Failed to parse account data as JSON.";
- return false;
+ case STATE_WORKING:
+ switch(m_stage) {
+ case MSAStage::Idle: {
+ QString loginMessage = tr("Logging in as %1 user");
+ if(m_data->type == AccountType::MSA) {
+ return loginMessage.arg("Microsoft");
+ }
+ else {
+ return loginMessage.arg("Mojang");
+ }
+ }
+ case MSAStage::UserAuth:
+ return tr("Logging in as XBox user");
+ case MSAStage::XboxAuth:
+ return tr("Logging in with XBox and Mojang services");
+ case MSAStage::MinecraftProfile:
+ return tr("Getting Minecraft profile");
+ case MSAStage::Skin:
+ return tr("Getting Minecraft skin");
+ default:
+ break;
+ }
+ default:
+ return AccountTask::getStateMessage();
}
- auto docObject = doc.object();
- m_account.msaToken = tokenFromJSON(docObject, "msa");
- m_account.userToken = tokenFromJSON(docObject, "utoken");
- m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main");
- m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc");
- m_account.minecraftToken = tokenFromJSON(docObject, "ygg");
-
- m_account.minecraftProfile = profileFromJSON(docObject, "profile");
-
- m_account.validity_ = m_account.minecraftProfile.validity;
-
- return true;
-}
-
-QByteArray Context::saveState() {
- QJsonDocument doc;
- QJsonObject output;
- tokenToJSON(output, m_account.msaToken, "msa");
- tokenToJSON(output, m_account.userToken, "utoken");
- tokenToJSON(output, m_account.xboxApiToken, "xrp-main");
- tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc");
- tokenToJSON(output, m_account.minecraftToken, "ygg");
- profileToJSON(output, m_account.minecraftProfile, "profile");
- doc.setObject(output);
- return doc.toJson(QJsonDocument::Indented);
}
diff --git a/launcher/minecraft/auth-msa/context.h b/launcher/minecraft/auth/flows/AuthContext.h
index f1ac99b8..5f99dba3 100644
--- a/launcher/minecraft/auth-msa/context.h
+++ b/launcher/minecraft/auth/flows/AuthContext.h
@@ -7,87 +7,47 @@
#include <QImage>
#include <katabasis/OAuth2.h>
+#include "Yggdrasil.h"
+#include "../AccountData.h"
+#include "../AccountTask.h"
-struct Skin {
- QString id;
- QString url;
- QString variant;
-
- QByteArray data;
-};
-
-struct Cape {
- QString id;
- QString url;
- QString alias;
-
- QByteArray data;
-};
-
-struct MinecraftProfile {
- QString id;
- QString name;
- Skin skin;
- int currentCape = -1;
- QVector<Cape> capes;
- Katabasis::Validity validity = Katabasis::Validity::None;
-};
-
-enum class AccountType {
- MSA,
- Mojang
-};
-
-struct AccountData {
- AccountType type = AccountType::MSA;
-
- Katabasis::Token msaToken;
- Katabasis::Token userToken;
- Katabasis::Token xboxApiToken;
- Katabasis::Token mojangservicesToken;
- Katabasis::Token minecraftToken;
-
- MinecraftProfile minecraftProfile;
- Katabasis::Validity validity_ = Katabasis::Validity::None;
-};
-
-class Context : public QObject
+class AuthContext : public AccountTask
{
Q_OBJECT
public:
- explicit Context(QObject *parent = 0);
-
- QByteArray saveState();
- bool resumeFromState(QByteArray data);
+ explicit AuthContext(AccountData * data, QObject *parent = 0);
bool isBusy() {
- return activity_ != Katabasis::Activity::Idle;
+ return m_activity != Katabasis::Activity::Idle;
};
Katabasis::Validity validity() {
- return m_account.validity_;
+ return m_data->validity_;
};
- bool signIn();
- bool silentSignIn();
- bool signOut();
+ //bool signOut();
+
+ QString getStateMessage() const override;
- QString userName();
- QString userId();
- QString gameToken();
signals:
- void succeeded();
- void failed();
void activityChanged(Katabasis::Activity activity);
private slots:
- void onLinkingSucceeded();
- void onLinkingFailed();
+// OAuth-specific callbacks
+ void onOAuthLinkingSucceeded();
+ void onOAuthLinkingFailed();
void onOpenBrowser(const QUrl &url);
void onCloseBrowser();
void onOAuthActivityChanged(Katabasis::Activity activity);
-private:
+// Yggdrasil specific callbacks
+ void onMojangSucceeded();
+ void onMojangFailed();
+
+protected:
+ void initMSA();
+ void initMojang();
+
void doUserAuth();
Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
@@ -109,20 +69,26 @@ private:
void checkResult();
-private:
+protected:
void beginActivity(Katabasis::Activity activity);
void finishActivity();
void clearTokens();
-private:
- Katabasis::OAuth2 *oauth2 = nullptr;
-
- int requestsDone = 0;
- bool xboxProfileSucceeded = false;
- bool mcAuthSucceeded = false;
- Katabasis::Activity activity_ = Katabasis::Activity::Idle;
-
- AccountData m_account;
+protected:
+ Katabasis::OAuth2 *m_oauth2 = nullptr;
+ Yggdrasil *m_yggdrasil = nullptr;
+
+ int m_requestsDone = 0;
+ bool m_xboxProfileSucceeded = false;
+ bool m_mcAuthSucceeded = false;
+ Katabasis::Activity m_activity = Katabasis::Activity::Idle;
+ enum class MSAStage {
+ Idle,
+ UserAuth,
+ XboxAuth,
+ MinecraftProfile,
+ Skin
+ } m_stage = MSAStage::Idle;
QNetworkAccessManager *mgr = nullptr;
};
diff --git a/launcher/minecraft/auth/flows/AuthenticateTask.cpp b/launcher/minecraft/auth/flows/AuthenticateTask.cpp
deleted file mode 100644
index 2e8dc859..00000000
--- a/launcher/minecraft/auth/flows/AuthenticateTask.cpp
+++ /dev/null
@@ -1,202 +0,0 @@
-
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "AuthenticateTask.h"
-#include "../MojangAccount.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QVariant>
-
-#include <QDebug>
-#include <QUuid>
-
-AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
- QObject *parent)
- : YggdrasilTask(account, parent), m_password(password)
-{
-}
-
-QJsonObject AuthenticateTask::getRequestContent() const
-{
- /*
- * {
- * "agent": { // optional
- * "name": "Minecraft", // So far this is the only encountered value
- * "version": 1 // This number might be increased
- * // by the vanilla client in the future
- * },
- * "username": "mojang account name", // Can be an email address or player name for
- // unmigrated accounts
- * "password": "mojang account password",
- * "clientToken": "client identifier" // optional
- * "requestUser": true/false // request the user structure
- * }
- */
- QJsonObject req;
-
- {
- QJsonObject agent;
- // C++ makes string literals void* for some stupid reason, so we have to tell it
- // QString... Thanks Obama.
- agent.insert("name", QString("Minecraft"));
- agent.insert("version", 1);
- req.insert("agent", agent);
- }
-
- req.insert("username", m_account->username());
- req.insert("password", m_password);
- req.insert("requestUser", true);
-
- // If we already have a client token, give it to the server.
- // Otherwise, let the server give us one.
-
- if(m_account->m_clientToken.isEmpty())
- {
- auto uuid = QUuid::createUuid();
- auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
- m_account->m_clientToken = uuidString;
- }
- req.insert("clientToken", m_account->m_clientToken);
-
- return req;
-}
-
-void AuthenticateTask::processResponse(QJsonObject responseData)
-{
- // Read the response data. We need to get the client token, access token, and the selected
- // profile.
- qDebug() << "Processing authentication response.";
- // qDebug() << responseData;
- // If we already have a client token, make sure the one the server gave us matches our
- // existing one.
- qDebug() << "Getting client token.";
- 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."));
- return;
- }
- if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
- return;
- }
- // Set the client token.
- m_account->m_clientToken = clientToken;
-
- // Now, we set the access token.
- qDebug() << "Getting access token.";
- 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."));
- return;
- }
- // Set the access token.
- m_account->m_accessToken = accessToken;
-
- // Now we load the list of available profiles.
- // Mojang hasn't yet implemented the profile system,
- // but we might as well support what's there so we
- // don't have trouble implementing it later.
- qDebug() << "Loading profile list.";
- QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
- QList<AccountProfile> loadedProfiles;
- for (auto iter : availableProfiles)
- {
- QJsonObject profile = iter.toObject();
- // Profiles are easy, we just need their ID and name.
- QString id = profile.value("id").toString("");
- QString name = profile.value("name").toString("");
- bool legacy = profile.value("legacy").toBool(false);
-
- if (id.isEmpty() || name.isEmpty())
- {
- // This should never happen, but we might as well
- // warn about it if it does so we can debug it easily.
- // You never know when Mojang might do something truly derpy.
- qWarning() << "Found entry in available profiles list with missing ID or name "
- "field. Ignoring it.";
- }
-
- // Now, add a new AccountProfile entry to the list.
- loadedProfiles.append({id, name, legacy});
- }
- // Put the list of profiles we loaded into the MojangAccount object.
- m_account->m_profiles = loadedProfiles;
-
- // Finally, we set the current profile to the correct value. This is pretty simple.
- // We do need to make sure that the current profile that the server gave us
- // is actually in the available profiles list.
- // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
- qDebug() << "Setting current profile.";
- QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
- QString currentProfileId = currentProfile.value("id").toString("");
- if (currentProfileId.isEmpty())
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
- return;
- }
- if (!m_account->setCurrentProfile(currentProfileId))
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
- return;
- }
-
- // this is what the vanilla launcher passes to the userProperties launch param
- if (responseData.contains("user"))
- {
- User u;
- auto obj = responseData.value("user").toObject();
- u.id = obj.value("id").toString();
- auto propArray = obj.value("properties").toArray();
- for (auto prop : propArray)
- {
- auto propTuple = prop.toObject();
- auto name = propTuple.value("name").toString();
- auto value = propTuple.value("value").toString();
- u.properties.insert(name, value);
- }
- m_account->m_user = u;
- }
-
- // 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);
-}
-
-QString AuthenticateTask::getEndpoint() const
-{
- return "authenticate";
-}
-
-QString AuthenticateTask::getStateMessage() const
-{
- switch (m_state)
- {
- case STATE_SENDING_REQUEST:
- return tr("Authenticating: Sending request...");
- case STATE_PROCESSING_RESPONSE:
- return tr("Authenticating: Processing response...");
- default:
- return YggdrasilTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/AuthenticateTask.h b/launcher/minecraft/auth/flows/AuthenticateTask.h
deleted file mode 100644
index 4c14eec7..00000000
--- a/launcher/minecraft/auth/flows/AuthenticateTask.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "../YggdrasilTask.h"
-
-#include <QObject>
-#include <QString>
-#include <QJsonObject>
-
-/**
- * The authenticate task takes a MojangAccount with no access token and password and attempts to
- * authenticate with Mojang's servers.
- * If successful, it will set the MojangAccount's access token.
- */
-class AuthenticateTask : public YggdrasilTask
-{
- Q_OBJECT
-public:
- AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
-
-protected:
- virtual QJsonObject getRequestContent() const override;
-
- virtual QString getEndpoint() const override;
-
- virtual void processResponse(QJsonObject responseData) override;
-
- virtual QString getStateMessage() const override;
-
-private:
- QString m_password;
-};
diff --git a/launcher/minecraft/auth/flows/MSAHelper.txt b/launcher/minecraft/auth/flows/MSAHelper.txt
new file mode 100644
index 00000000..dfaec374
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSAHelper.txt
@@ -0,0 +1,51 @@
+class Helper : public QObject {
+ Q_OBJECT
+
+public:
+ Helper(MSAFlows * context) : QObject(), context_(context), msg_(QString()) {
+ QFile tokenCache("usercache.dat");
+ if(tokenCache.open(QIODevice::ReadOnly)) {
+ context_->resumeFromState(tokenCache.readAll());
+ }
+ }
+
+public slots:
+ void run() {
+ connect(context_, &MSAFlows::activityChanged, this, &Helper::onActivityChanged);
+ context_->silentSignIn();
+ }
+
+ void onFailed() {
+ qDebug() << "Login failed";
+ }
+
+ void onActivityChanged(Katabasis::Activity activity) {
+ if(activity == Katabasis::Activity::Idle) {
+ switch(context_->validity()) {
+ case Katabasis::Validity::None: {
+ // account is gone, remove it.
+ QFile::remove("usercache.dat");
+ }
+ break;
+ case Katabasis::Validity::Assumed: {
+ // this is basically a soft-failed refresh. do nothing.
+ }
+ break;
+ case Katabasis::Validity::Certain: {
+ // stuff got refreshed / signed in. Save.
+ auto data = context_->saveState();
+ QSaveFile tokenCache("usercache.dat");
+ if(tokenCache.open(QIODevice::WriteOnly)) {
+ tokenCache.write(context_->saveState());
+ tokenCache.commit();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+private:
+ MSAFlows *context_;
+ QString msg_;
+};
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp
new file mode 100644
index 00000000..03beb279
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp
@@ -0,0 +1,20 @@
+#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_oauth2->unlink();
+ *m_data = AccountData();
+ m_oauth2->link();
+}
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h
new file mode 100644
index 00000000..9556f254
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSAInteractive.h
@@ -0,0 +1,10 @@
+#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
new file mode 100644
index 00000000..8ce43c1f
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSASilent.cpp
@@ -0,0 +1,16 @@
+#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
new file mode 100644
index 00000000..e1b3d43d
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSASilent.h
@@ -0,0 +1,10 @@
+#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/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp
new file mode 100644
index 00000000..cca911b5
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangLogin.cpp
@@ -0,0 +1,14 @@
+#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
new file mode 100644
index 00000000..2e765ae8
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangLogin.h
@@ -0,0 +1,13 @@
+#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
new file mode 100644
index 00000000..af99175c
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangRefresh.cpp
@@ -0,0 +1,14 @@
+#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
new file mode 100644
index 00000000..fb4facd5
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangRefresh.h
@@ -0,0 +1,10 @@
+#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/flows/RefreshTask.cpp b/launcher/minecraft/auth/flows/RefreshTask.cpp
deleted file mode 100644
index ecba178d..00000000
--- a/launcher/minecraft/auth/flows/RefreshTask.cpp
+++ /dev/null
@@ -1,144 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "RefreshTask.h"
-#include "../MojangAccount.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QVariant>
-
-#include <QDebug>
-
-RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
-{
-}
-
-QJsonObject RefreshTask::getRequestContent() const
-{
- /*
- * {
- * "clientToken": "client identifier"
- * "accessToken": "current access token to be refreshed"
- * "selectedProfile": // specifying this causes errors
- * {
- * "id": "profile ID"
- * "name": "profile name"
- * }
- * "requestUser": true/false // request the user structure
- * }
- */
- QJsonObject req;
- req.insert("clientToken", m_account->m_clientToken);
- req.insert("accessToken", m_account->m_accessToken);
- /*
- {
- auto currentProfile = m_account->currentProfile();
- QJsonObject profile;
- profile.insert("id", currentProfile->id());
- profile.insert("name", currentProfile->name());
- req.insert("selectedProfile", profile);
- }
- */
- req.insert("requestUser", true);
-
- return req;
-}
-
-void RefreshTask::processResponse(QJsonObject responseData)
-{
- // Read the response data. We need to get the client token, access token, and the selected
- // profile.
- qDebug() << "Processing authentication response.";
-
- // qDebug() << responseData;
- // If we already have a client token, make sure the one the server gave us matches our
- // existing one.
- 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."));
- return;
- }
- if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
- return;
- }
-
- // Now, we set the access token.
- qDebug() << "Getting new access token.";
- 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."));
- return;
- }
-
- // we validate that the server responded right. (our current profile = returned current
- // profile)
- QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
- QString currentProfileId = currentProfile.value("id").toString("");
- if (m_account->currentProfile()->id != currentProfileId)
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
- return;
- }
-
- // this is what the vanilla launcher passes to the userProperties launch param
- if (responseData.contains("user"))
- {
- User u;
- auto obj = responseData.value("user").toObject();
- u.id = obj.value("id").toString();
- auto propArray = obj.value("properties").toArray();
- for (auto prop : propArray)
- {
- auto propTuple = prop.toObject();
- auto name = propTuple.value("name").toString();
- auto value = propTuple.value("value").toString();
- u.properties.insert(name, value);
- }
- m_account->m_user = u;
- }
-
- // We've made it through the minefield of possible errors. Return true to indicate that
- // we've succeeded.
- qDebug() << "Finished reading refresh response.";
- // Reset the access token.
- m_account->m_accessToken = accessToken;
- changeState(STATE_SUCCEEDED);
-}
-
-QString RefreshTask::getEndpoint() const
-{
- return "refresh";
-}
-
-QString RefreshTask::getStateMessage() const
-{
- switch (m_state)
- {
- case STATE_SENDING_REQUEST:
- return tr("Refreshing login token...");
- case STATE_PROCESSING_RESPONSE:
- return tr("Refreshing login token: Processing response...");
- default:
- return YggdrasilTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/RefreshTask.h b/launcher/minecraft/auth/flows/RefreshTask.h
deleted file mode 100644
index f0840dda..00000000
--- a/launcher/minecraft/auth/flows/RefreshTask.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "../YggdrasilTask.h"
-
-#include <QObject>
-#include <QString>
-#include <QJsonObject>
-
-/**
- * The authenticate task takes a MojangAccount with a possibly timed-out access token
- * and attempts to authenticate with Mojang's servers.
- * If successful, it will set the new access token. The token is considered validated.
- */
-class RefreshTask : public YggdrasilTask
-{
- Q_OBJECT
-public:
- RefreshTask(MojangAccount * account);
-
-protected:
- virtual QJsonObject getRequestContent() const override;
-
- virtual QString getEndpoint() const override;
-
- virtual void processResponse(QJsonObject responseData) override;
-
- virtual QString getStateMessage() const override;
-};
-
diff --git a/launcher/minecraft/auth/flows/ValidateTask.cpp b/launcher/minecraft/auth/flows/ValidateTask.cpp
deleted file mode 100644
index 6b3f0a65..00000000
--- a/launcher/minecraft/auth/flows/ValidateTask.cpp
+++ /dev/null
@@ -1,61 +0,0 @@
-
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "ValidateTask.h"
-#include "../MojangAccount.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QVariant>
-
-#include <QDebug>
-
-ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
- : YggdrasilTask(account, parent)
-{
-}
-
-QJsonObject ValidateTask::getRequestContent() const
-{
- QJsonObject req;
- req.insert("accessToken", m_account->m_accessToken);
- return req;
-}
-
-void ValidateTask::processResponse(QJsonObject responseData)
-{
- // Assume that if processError wasn't called, then the request was successful.
- changeState(YggdrasilTask::STATE_SUCCEEDED);
-}
-
-QString ValidateTask::getEndpoint() const
-{
- return "validate";
-}
-
-QString ValidateTask::getStateMessage() const
-{
- switch (m_state)
- {
- case YggdrasilTask::STATE_SENDING_REQUEST:
- return tr("Validating access token: Sending request...");
- case YggdrasilTask::STATE_PROCESSING_RESPONSE:
- return tr("Validating access token: Processing response...");
- default:
- return YggdrasilTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/ValidateTask.h b/launcher/minecraft/auth/flows/ValidateTask.h
deleted file mode 100644
index 986c2e9f..00000000
--- a/launcher/minecraft/auth/flows/ValidateTask.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
- */
-
-#pragma once
-
-#include "../YggdrasilTask.h"
-
-#include <QObject>
-#include <QString>
-#include <QJsonObject>
-
-/**
- * The validate task takes a MojangAccount and checks to make sure its access token is valid.
- */
-class ValidateTask : public YggdrasilTask
-{
- Q_OBJECT
-public:
- ValidateTask(MojangAccount *account, QObject *parent = 0);
-
-protected:
- virtual QJsonObject getRequestContent() const override;
-
- virtual QString getEndpoint() const override;
-
- virtual void processResponse(QJsonObject responseData) override;
-
- virtual QString getStateMessage() const override;
-
-private:
-};
diff --git a/launcher/minecraft/auth/YggdrasilTask.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp
index 0857b46b..7cea059c 100644
--- a/launcher/minecraft/auth/YggdrasilTask.cpp
+++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp
@@ -13,8 +13,8 @@
* limitations under the License.
*/
-#include "YggdrasilTask.h"
-#include "MojangAccount.h"
+#include "Yggdrasil.h"
+#include "../AccountData.h"
#include <QObject>
#include <QString>
@@ -29,68 +29,147 @@
#include <QDebug>
-YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
- : Task(parent), m_account(account)
+Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
+ : AccountTask(data, parent)
{
changeState(STATE_CREATED);
}
-void YggdrasilTask::executeTask()
-{
- changeState(STATE_SENDING_REQUEST);
-
- // Get the content of the request we're going to send to the server.
- QJsonDocument doc(getRequestContent());
+void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
+ changeState(STATE_WORKING);
- QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint());
- QNetworkRequest netRequest(reqUrl);
+ QNetworkRequest netRequest(endpoint);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-
- QByteArray requestData = doc.toJson();
- m_netReply = ENV.qnam().post(netRequest, requestData);
- connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
- connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
- connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
- connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors);
+ m_netReply = ENV.qnam().post(netRequest, content);
+ connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
+ connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
+ connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
+ connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
timeout_keeper.setSingleShot(true);
timeout_keeper.start(timeout_max);
counter.setSingleShot(false);
counter.start(time_step);
progress(0, timeout_max);
- connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout);
- connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat);
+ connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout);
+ connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
+}
+
+void Yggdrasil::executeTask() {
+}
+
+void Yggdrasil::refresh() {
+ start();
+ /*
+ * {
+ * "clientToken": "client identifier"
+ * "accessToken": "current access token to be refreshed"
+ * "selectedProfile": // specifying this causes errors
+ * {
+ * "id": "profile ID"
+ * "name": "profile name"
+ * }
+ * "requestUser": true/false // request the user structure
+ * }
+ */
+ QJsonObject req;
+ req.insert("clientToken", m_data->clientToken());
+ req.insert("accessToken", m_data->accessToken());
+ /*
+ {
+ auto currentProfile = m_account->currentProfile();
+ QJsonObject profile;
+ profile.insert("id", currentProfile->id());
+ profile.insert("name", currentProfile->name());
+ req.insert("selectedProfile", profile);
+ }
+ */
+ req.insert("requestUser", false);
+ QJsonDocument doc(req);
+
+ QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh");
+ QByteArray requestData = doc.toJson();
+
+ sendRequest(reqUrl, requestData);
+}
+
+void Yggdrasil::login(QString password) {
+ start();
+ /*
+ * {
+ * "agent": { // optional
+ * "name": "Minecraft", // So far this is the only encountered value
+ * "version": 1 // This number might be increased
+ * // by the vanilla client in the future
+ * },
+ * "username": "mojang account name", // Can be an email address or player name for
+ * // unmigrated accounts
+ * "password": "mojang account password",
+ * "clientToken": "client identifier", // optional
+ * "requestUser": true/false // request the user structure
+ * }
+ */
+ QJsonObject req;
+
+ {
+ QJsonObject agent;
+ // C++ makes string literals void* for some stupid reason, so we have to tell it
+ // QString... Thanks Obama.
+ agent.insert("name", QString("Minecraft"));
+ agent.insert("version", 1);
+ req.insert("agent", agent);
+ }
+
+ req.insert("username", m_data->userName());
+ req.insert("password", password);
+ req.insert("requestUser", false);
+
+ // If we already have a client token, give it to the server.
+ // Otherwise, let the server give us one.
+
+ m_data->generateClientTokenIfMissing();
+ req.insert("clientToken", m_data->clientToken());
+
+ QJsonDocument doc(req);
+
+ QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate");
+ QNetworkRequest netRequest(reqUrl);
+ QByteArray requestData = doc.toJson();
+
+ sendRequest(reqUrl, requestData);
}
-void YggdrasilTask::refreshTimers(qint64, qint64)
+
+
+void Yggdrasil::refreshTimers(qint64, qint64)
{
timeout_keeper.stop();
timeout_keeper.start(timeout_max);
progress(count = 0, timeout_max);
}
-void YggdrasilTask::heartbeat()
+void Yggdrasil::heartbeat()
{
count += time_step;
progress(count, timeout_max);
}
-bool YggdrasilTask::abort()
+bool Yggdrasil::abort()
{
progress(timeout_max, timeout_max);
// TODO: actually use this in a meaningful way
- m_aborted = YggdrasilTask::BY_USER;
+ m_aborted = Yggdrasil::BY_USER;
m_netReply->abort();
return true;
}
-void YggdrasilTask::abortByTimeout()
+void Yggdrasil::abortByTimeout()
{
progress(timeout_max, timeout_max);
// TODO: actually use this in a meaningful way
- m_aborted = YggdrasilTask::BY_TIMEOUT;
+ m_aborted = Yggdrasil::BY_TIMEOUT;
m_netReply->abort();
}
-void YggdrasilTask::sslErrors(QList<QSslError> errors)
+void Yggdrasil::sslErrors(QList<QSslError> errors)
{
int i = 1;
for (auto error : errors)
@@ -102,9 +181,52 @@ void YggdrasilTask::sslErrors(QList<QSslError> errors)
}
}
-void YggdrasilTask::processReply()
+void Yggdrasil::processResponse(QJsonObject responseData)
+{
+ // Read the response data. We need to get the client token, access token, and the selected
+ // profile.
+ qDebug() << "Processing authentication response.";
+
+ // qDebug() << responseData;
+ // If we already have a client token, make sure the one the server gave us matches our
+ // existing one.
+ 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."));
+ 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."));
+ return;
+ }
+
+ // Now, we set the access token.
+ qDebug() << "Getting access token.";
+ 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."));
+ return;
+ }
+ // Set the access token.
+ m_data->yggdrasilToken.token = accessToken;
+ m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
+
+ // 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);
+}
+
+void Yggdrasil::processReply()
{
- changeState(STATE_PROCESSING_RESPONSE);
+ changeState(STATE_WORKING);
switch (m_netReply->error())
{
@@ -195,7 +317,7 @@ void YggdrasilTask::processReply()
}
}
-void YggdrasilTask::processError(QJsonObject responseData)
+void Yggdrasil::processError(QJsonObject responseData)
{
QJsonValue errorVal = responseData.value("error");
QJsonValue errorMessageValue = responseData.value("errorMessage");
@@ -213,43 +335,3 @@ void YggdrasilTask::processError(QJsonObject responseData)
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
}
}
-
-QString YggdrasilTask::getStateMessage() const
-{
- switch (m_state)
- {
- case STATE_CREATED:
- return "Waiting...";
- case STATE_SENDING_REQUEST:
- return tr("Sending request to auth servers...");
- case STATE_PROCESSING_RESPONSE:
- return tr("Processing response from servers...");
- case STATE_SUCCEEDED:
- return tr("Authentication task succeeded.");
- case STATE_FAILED_SOFT:
- return tr("Failed to contact the authentication server.");
- case STATE_FAILED_HARD:
- return tr("Failed to authenticate.");
- default:
- return tr("...");
- }
-}
-
-void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason)
-{
- m_state = newState;
- setStatus(getStateMessage());
- if (newState == STATE_SUCCEEDED)
- {
- emitSucceeded();
- }
- else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
- {
- emitFailed(reason);
- }
-}
-
-YggdrasilTask::State YggdrasilTask::state()
-{
- return m_state;
-}
diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/flows/Yggdrasil.h
new file mode 100644
index 00000000..e709cb9f
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Yggdrasil.h
@@ -0,0 +1,82 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "../AccountTask.h"
+
+#include <QString>
+#include <QJsonObject>
+#include <QTimer>
+#include <qsslerror.h>
+
+#include "../MinecraftAccount.h"
+
+class QNetworkReply;
+
+/**
+ * A Yggdrasil task is a task that performs an operation on a given mojang account.
+ */
+class Yggdrasil : public AccountTask
+{
+ Q_OBJECT
+public:
+ explicit Yggdrasil(AccountData * data, QObject *parent = 0);
+ virtual ~Yggdrasil() {};
+
+ void refresh();
+ void login(QString password);
+protected:
+ void executeTask() override;
+
+ /**
+ * Processes the response received from the server.
+ * If an error occurred, this should emit a failed signal.
+ * If Yggdrasil gave an error response, it should call setError() first, and then return false.
+ * Otherwise, it should return true.
+ * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
+ * an empty QJsonObject.
+ */
+ void processResponse(QJsonObject responseData);
+
+ /**
+ * Processes an error response received from the server.
+ * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
+ * \returns a QString error message that will be passed to emitFailed.
+ */
+ virtual void processError(QJsonObject responseData);
+
+protected slots:
+ void processReply();
+ void refreshTimers(qint64, qint64);
+ void heartbeat();
+ void sslErrors(QList<QSslError>);
+ void abortByTimeout();
+
+public slots:
+ virtual bool abort() override;
+
+private:
+ void sendRequest(QUrl endpoint, QByteArray content);
+
+protected:
+ QNetworkReply *m_netReply = nullptr;
+ QTimer timeout_keeper;
+ QTimer counter;
+ int count = 0; // num msec since time reset
+
+ const int timeout_max = 30000;
+ const int time_step = 50;
+};
diff --git a/launcher/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h
index c5bd75f3..cb4de23f 100644
--- a/launcher/minecraft/launch/ClaimAccount.h
+++ b/launcher/minecraft/launch/ClaimAccount.h
@@ -16,7 +16,7 @@
#pragma once
#include <launch/LaunchStep.h>
-#include <minecraft/auth/MojangAccount.h>
+#include <minecraft/auth/MinecraftAccount.h>
class ClaimAccount: public LaunchStep
{
@@ -33,5 +33,5 @@ public:
}
private:
std::unique_ptr<UseLock> lock;
- MojangAccountPtr m_account;
+ MinecraftAccountPtr m_account;
};
diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp
index ff3736ed..a3cd86a4 100644
--- a/launcher/pages/global/AccountListPage.cpp
+++ b/launcher/pages/global/AccountListPage.cpp
@@ -29,12 +29,13 @@
#include "dialogs/CustomMessageBox.h"
#include "dialogs/SkinUploadDialog.h"
#include "tasks/Task.h"
-#include "minecraft/auth/YggdrasilTask.h"
+#include "minecraft/auth/AccountTask.h"
#include "minecraft/services/SkinDelete.h"
#include "MultiMC.h"
#include "BuildConfig.h"
+#include <dialogs/MSALoginDialog.h>
AccountListPage::AccountListPage(QWidget *parent)
: QMainWindow(parent), ui(new Ui::AccountListPage)
@@ -50,11 +51,12 @@ AccountListPage::AccountListPage(QWidget *parent)
m_accounts = MMC->accounts();
ui->listView->setModel(m_accounts.get());
- ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
// Expand the account column
- ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
QItemSelectionModel *selectionModel = ui->listView->selectionModel();
@@ -63,8 +65,8 @@ AccountListPage::AccountListPage(QWidget *parent)
});
connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu);
- connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged()));
- connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged()));
+ connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged);
+ connect(m_accounts.get(), &AccountList::activeAccountChanged, this, &AccountListPage::listChanged);
updateButtonStates();
}
@@ -103,9 +105,36 @@ void AccountListPage::listChanged()
updateButtonStates();
}
-void AccountListPage::on_actionAdd_triggered()
+void AccountListPage::on_actionAddMojang_triggered()
{
- addAccount(tr("Please enter your Minecraft account email and password to add your account."));
+ MinecraftAccountPtr account = LoginDialog::newAccount(
+ this,
+ tr("Please enter your Mojang account email and password to add your account.")
+ );
+
+ if (account != nullptr)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1) {
+ m_accounts->setActiveAccount(account->profileId());
+ }
+ }
+}
+
+void AccountListPage::on_actionAddMicrosoft_triggered()
+{
+ MinecraftAccountPtr account = MSALoginDialog::newAccount(
+ this,
+ tr("Please enter your Mojang account email and password to add your account.")
+ );
+
+ if (account != nullptr)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1) {
+ m_accounts->setActiveAccount(account->profileId());
+ }
+ }
}
void AccountListPage::on_actionRemove_triggered()
@@ -124,9 +153,8 @@ void AccountListPage::on_actionSetDefault_triggered()
if (selection.size() > 0)
{
QModelIndex selected = selection.first();
- MojangAccountPtr account =
- selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
- m_accounts->setActiveAccount(account->username());
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ m_accounts->setActiveAccount(account->profileId());
}
}
@@ -156,39 +184,13 @@ void AccountListPage::updateButtonStates()
}
-void AccountListPage::addAccount(const QString &errMsg)
-{
- // TODO: The login dialog isn't quite done yet
- MojangAccountPtr account = LoginDialog::newAccount(this, errMsg);
-
- if (account != nullptr)
- {
- m_accounts->addAccount(account);
- if (m_accounts->count() == 1)
- m_accounts->setActiveAccount(account->username());
-
- // Grab associated player skins
- auto job = new NetJob("Player skins: " + account->username());
-
- for (AccountProfile profile : account->profiles())
- {
- auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png");
- auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta);
- job->addNetAction(action);
- meta->setStale(true);
- }
-
- job->start();
- }
-}
-
void AccountListPage::on_actionUploadSkin_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() > 0)
{
QModelIndex selected = selection.first();
- MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
SkinUploadDialog dialog(account, this);
dialog.exec();
}
@@ -202,8 +204,8 @@ void AccountListPage::on_actionDeleteSkin_triggered()
QModelIndex selected = selection.first();
AuthSessionPtr session = std::make_shared<AuthSession>();
- MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
- auto login = account->login(session);
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ auto login = account->refresh(session);
ProgressDialog prog(this);
if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec();
diff --git a/launcher/pages/global/AccountListPage.h b/launcher/pages/global/AccountListPage.h
index fba1833f..24bb96da 100644
--- a/launcher/pages/global/AccountListPage.h
+++ b/launcher/pages/global/AccountListPage.h
@@ -20,7 +20,7 @@
#include "pages/BasePage.h"
-#include "minecraft/auth/MojangAccountList.h"
+#include "minecraft/auth/AccountList.h"
#include "MultiMC.h"
namespace Ui
@@ -60,7 +60,8 @@ public:
}
public slots:
- void on_actionAdd_triggered();
+ void on_actionAddMojang_triggered();
+ void on_actionAddMicrosoft_triggered();
void on_actionRemove_triggered();
void on_actionSetDefault_triggered();
void on_actionNoDefault_triggered();
@@ -74,11 +75,10 @@ public slots:
protected slots:
void ShowContextMenu(const QPoint &pos);
- void addAccount(const QString& errMsg="");
private:
void changeEvent(QEvent * event) override;
QMenu * createPopupMenu() override;
- std::shared_ptr<MojangAccountList> m_accounts;
+ std::shared_ptr<AccountList> m_accounts;
Ui::AccountListPage *ui;
};
diff --git a/launcher/pages/global/AccountListPage.ui b/launcher/pages/global/AccountListPage.ui
index 71647db3..887c3d48 100644
--- a/launcher/pages/global/AccountListPage.ui
+++ b/launcher/pages/global/AccountListPage.ui
@@ -25,7 +25,23 @@
<number>0</number>
</property>
<item>
- <widget class="VersionListView" name="listView"/>
+ <widget class="VersionListView" name="listView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="allColumnsShowFocus">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
</item>
</layout>
</widget>
@@ -36,7 +52,8 @@
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
- <addaction name="actionAdd"/>
+ <addaction name="actionAddMicrosoft"/>
+ <addaction name="actionAddMojang"/>
<addaction name="actionRemove"/>
<addaction name="actionSetDefault"/>
<addaction name="actionNoDefault"/>
@@ -44,9 +61,9 @@
<addaction name="actionUploadSkin"/>
<addaction name="actionDeleteSkin"/>
</widget>
- <action name="actionAdd">
+ <action name="actionAddMojang">
<property name="text">
- <string>Add</string>
+ <string>Add Mojang</string>
</property>
</action>
<action name="actionRemove">
@@ -80,6 +97,11 @@
<string>Delete the currently active skin and go back to the default one</string>
</property>
</action>
+ <action name="actionAddMicrosoft">
+ <property name="text">
+ <string>Add Microsoft</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
diff --git a/launcher/pages/instance/VersionPage.cpp b/launcher/pages/instance/VersionPage.cpp
index a98bfb7d..20cb2c9f 100644
--- a/launcher/pages/instance/VersionPage.cpp
+++ b/launcher/pages/instance/VersionPage.cpp
@@ -38,7 +38,7 @@
#include <QUrl>
#include "minecraft/PackProfile.h"
-#include "minecraft/auth/MojangAccountList.h"
+#include "minecraft/auth/AccountList.h"
#include "minecraft/mod/Mod.h"
#include "icons/IconList.h"
#include "Exception.h"