From cf75b6427d8212bd0a24d3beca46e247dc9dcbd6 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Fri, 2 Dec 2022 23:00:25 -0500
Subject: [PATCH 1/6] 'Custom Yggdrasil' AccountType

Signed-off-by: Evan Goode <mail@evangoo.de>
---
 buildconfig/BuildConfig.h                     |   8 +-
 launcher/CMakeLists.txt                       |   5 +
 launcher/minecraft/MinecraftInstance.cpp      |  14 ++
 launcher/minecraft/MinecraftInstance.h        |   2 +
 launcher/minecraft/auth/AccountData.cpp       |  63 +++++++-
 launcher/minecraft/auth/AccountData.h         |  12 ++
 launcher/minecraft/auth/AccountList.cpp       |   4 +-
 launcher/minecraft/auth/AuthSession.h         |   7 +
 launcher/minecraft/auth/MinecraftAccount.cpp  |  39 +++++
 launcher/minecraft/auth/MinecraftAccount.h    |  60 +++++++
 launcher/minecraft/auth/Yggdrasil.cpp         |   7 +-
 .../minecraft/auth/flows/CustomYggdrasil.cpp  |  25 +++
 .../minecraft/auth/flows/CustomYggdrasil.h    |  26 +++
 .../auth/steps/MinecraftProfileStep.cpp       |   4 +-
 .../auth/steps/MinecraftProfileStepMojang.cpp |   9 +-
 .../minecraft/launch/DirectJavaLaunch.cpp     |   2 +
 .../minecraft/launch/LauncherPartLaunch.cpp   |   3 +
 launcher/minecraft/services/CapeChange.cpp    |  14 +-
 launcher/minecraft/services/CapeChange.h      |   5 +-
 launcher/minecraft/services/SkinUpload.cpp    |   9 +-
 launcher/minecraft/services/SkinUpload.h      |   5 +-
 .../ui/dialogs/CustomYggdrasilLoginDialog.cpp | 148 ++++++++++++++++++
 .../ui/dialogs/CustomYggdrasilLoginDialog.h   |  60 +++++++
 .../ui/dialogs/CustomYggdrasilLoginDialog.ui  | 105 +++++++++++++
 launcher/ui/dialogs/SkinUploadDialog.cpp      |   4 +-
 launcher/ui/pages/global/AccountListPage.cpp  |  30 ++++
 launcher/ui/pages/global/AccountListPage.h    |   1 +
 launcher/ui/pages/global/AccountListPage.ui   |   6 +
 28 files changed, 646 insertions(+), 31 deletions(-)
 create mode 100644 launcher/minecraft/auth/flows/CustomYggdrasil.cpp
 create mode 100644 launcher/minecraft/auth/flows/CustomYggdrasil.h
 create mode 100644 launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp
 create mode 100644 launcher/ui/dialogs/CustomYggdrasilLoginDialog.h
 create mode 100644 launcher/ui/dialogs/CustomYggdrasilLoginDialog.ui

diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index 8543d724..ecb9335d 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -142,7 +142,13 @@ class Config {
 
     QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
     QString LIBRARY_BASE = "https://libraries.minecraft.net/";
-    QString AUTH_BASE = "https://authserver.mojang.com/";
+
+    // Minecraft expects these without trailing slashes, best to keep that format everywhere
+    QString MOJANG_AUTH_BASE = "https://authserver.mojang.com";
+    QString MOJANG_ACCOUNT_BASE = "https://api.mojang.com";
+    QString MOJANG_SESSION_BASE = "https://sessionserver.mojang.com";
+    QString MOJANG_SERVICES_BASE = "https://api.minecraftservices.com";
+
     QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
     QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/";  // FIXME: move into CMakeLists
     QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/";  // FIXME: move into CMakeLists
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index ce2771a4..9eac6a7a 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -212,6 +212,8 @@ set(MINECRAFT_SOURCES
 
     minecraft/auth/flows/AuthFlow.cpp
     minecraft/auth/flows/AuthFlow.h
+    minecraft/auth/flows/CustomYggdrasil.cpp
+    minecraft/auth/flows/CustomYggdrasil.h
     minecraft/auth/flows/Mojang.cpp
     minecraft/auth/flows/Mojang.h
     minecraft/auth/flows/MSA.cpp
@@ -914,6 +916,8 @@ SET(LAUNCHER_SOURCES
     ui/dialogs/ImportResourceDialog.h
     ui/dialogs/LoginDialog.cpp
     ui/dialogs/LoginDialog.h
+    ui/dialogs/CustomYggdrasilLoginDialog.cpp
+    ui/dialogs/CustomYggdrasilLoginDialog.h
     ui/dialogs/MSALoginDialog.cpp
     ui/dialogs/MSALoginDialog.h
     ui/dialogs/OfflineLoginDialog.cpp
@@ -1061,6 +1065,7 @@ qt_wrap_ui(LAUNCHER_UI
     ui/dialogs/OfflineLoginDialog.ui
     ui/dialogs/AboutDialog.ui
     ui/dialogs/LoginDialog.ui
+    ui/dialogs/CustomYggdrasilLoginDialog.ui
     ui/dialogs/EditAccountDialog.ui
     ui/dialogs/ReviewMessageBox.ui
     ui/dialogs/ScrollMessageBox.ui
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index f8ed5214..f6a7d111 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -462,6 +462,20 @@ QString MinecraftInstance::getLauncher()
     return "standard";
 }
 
+QStringList MinecraftInstance::processAuthArgs(AuthSessionPtr session) const
+{
+    QStringList args;
+    if(session->uses_custom_api_servers)
+    {
+        args << "-Dminecraft.api.env=custom";
+        args << "-Dminecraft.api.auth.host=" + session->auth_server_url;
+        args << "-Dminecraft.api.account.host=" + session->account_server_url;
+        args << "-Dminecraft.api.session.host=" + session->session_server_url;
+        args << "-Dminecraft.api.services.host=" + session->services_server_url;
+    }
+    return args;
+}
+
 QMap<QString, QString> MinecraftInstance::getVariables()
 {
     QMap<QString, QString> out;
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
index 068b3008..cba0a612 100644
--- a/launcher/minecraft/MinecraftInstance.h
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -158,6 +158,8 @@ public:
     // FIXME: remove
     virtual QString getMainClass() const;
 
+    virtual QStringList processAuthArgs(AuthSessionPtr account) const;
+
     // FIXME: remove
     virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
 
diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp
index 44f7e256..1f32efd4 100644
--- a/launcher/minecraft/auth/AccountData.cpp
+++ b/launcher/minecraft/auth/AccountData.cpp
@@ -34,6 +34,8 @@
  */
 
 #include "AccountData.h"
+#include "BuildConfig.h"
+
 #include <QJsonDocument>
 #include <QJsonObject>
 #include <QJsonArray>
@@ -350,6 +352,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
         type = AccountType::MSA;
     } else if (typeS == "Mojang") {
         type = AccountType::Mojang;
+    } else if (typeS == "CustomYggdrasil") {
+        type = AccountType::CustomYggdrasil;
     } else if (typeS == "Offline") {
         type = AccountType::Offline;
     } else {
@@ -362,6 +366,13 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
         canMigrateToMSA = data.value("canMigrateToMSA").toBool(false);
     }
 
+    if(type == AccountType::CustomYggdrasil) {
+        customAuthServerUrl = data.value("customAuthServerUrl").toString();
+        customAccountServerUrl = data.value("customAccountServerUrl").toString();
+        customSessionServerUrl = data.value("customSessionServerUrl").toString();
+        customServicesServerUrl = data.value("customServicesServerUrl").toString();
+    }
+
     if(type == AccountType::MSA) {
         auto clientIDV = data.value("msa-client-id");
         if (clientIDV.isString()) {
@@ -406,6 +417,13 @@ QJsonObject AccountData::saveState() const {
         tokenToJSONV3(output, xboxApiToken, "xrp-main");
         tokenToJSONV3(output, mojangservicesToken, "xrp-mc");
     }
+    else if (type == AccountType::CustomYggdrasil) {
+        output["type"] = "CustomYggdrasil";
+        output["customAuthServerUrl"] = customAuthServerUrl;
+        output["customAccountServerUrl"] = customAccountServerUrl;
+        output["customSessionServerUrl"] = customSessionServerUrl;
+        output["customServicesServerUrl"] = customServicesServerUrl;
+    }
     else if (type == AccountType::Offline) {
         output["type"] = "Offline";
     }
@@ -416,6 +434,42 @@ QJsonObject AccountData::saveState() const {
     return output;
 }
 
+bool AccountData::usesCustomApiServers() const {
+    return type == AccountType::CustomYggdrasil;
+}
+
+QString AccountData::authServerUrl() const {
+    if(usesCustomApiServers()) {
+        return customAuthServerUrl;
+    } else {
+        return BuildConfig.MOJANG_AUTH_BASE;
+    }
+}
+
+QString AccountData::accountServerUrl() const {
+    if(usesCustomApiServers()) {
+        return customAccountServerUrl;
+    } else {
+        return BuildConfig.MOJANG_ACCOUNT_BASE;
+    }
+}
+
+QString AccountData::sessionServerUrl() const {
+    if(usesCustomApiServers()) {
+        return customSessionServerUrl;
+    } else {
+        return BuildConfig.MOJANG_SESSION_BASE;
+    }
+}
+
+QString AccountData::servicesServerUrl() const {
+    if(usesCustomApiServers()) {
+        return customServicesServerUrl;
+    } else {
+        return BuildConfig.MOJANG_SERVICES_BASE;
+    }
+}
+
 QString AccountData::userName() const {
     if(type == AccountType::MSA) {
         return QString();
@@ -428,14 +482,14 @@ QString AccountData::accessToken() const {
 }
 
 QString AccountData::clientToken() const {
-    if(type != AccountType::Mojang) {
+    if(type != AccountType::Mojang && type != AccountType::CustomYggdrasil) {
         return QString();
     }
     return yggdrasilToken.extra["clientToken"].toString();
 }
 
 void AccountData::setClientToken(QString clientToken) {
-    if(type != AccountType::Mojang) {
+    if(type != AccountType::Mojang && type != AccountType::CustomYggdrasil) {
         return;
     }
     yggdrasilToken.extra["clientToken"] = clientToken;
@@ -449,7 +503,7 @@ void AccountData::generateClientTokenIfMissing() {
 }
 
 void AccountData::invalidateClientToken() {
-    if(type != AccountType::Mojang) {
+    if(type != AccountType::Mojang && type != AccountType::CustomYggdrasil) {
         return;
     }
     yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]"));
@@ -473,6 +527,9 @@ QString AccountData::accountDisplayString() const {
         case AccountType::Mojang: {
             return userName();
         }
+        case AccountType::CustomYggdrasil: {
+            return userName();
+        }
         case AccountType::Offline: {
             return QObject::tr("<Offline>");
         }
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h
index 092e1691..0d7c01df 100644
--- a/launcher/minecraft/auth/AccountData.h
+++ b/launcher/minecraft/auth/AccountData.h
@@ -74,6 +74,7 @@ struct MinecraftProfile {
 enum class AccountType {
     MSA,
     Mojang,
+    CustomYggdrasil,
     Offline
 };
 
@@ -93,6 +94,12 @@ struct AccountData {
     bool resumeStateFromV2(QJsonObject data);
     bool resumeStateFromV3(QJsonObject data);
 
+    bool usesCustomApiServers() const;
+    QString authServerUrl() const;
+    QString accountServerUrl() const;
+    QString sessionServerUrl() const;
+    QString servicesServerUrl() const;
+
     //! userName for Mojang accounts, gamertag for MSA
     QString accountDisplayString() const;
 
@@ -117,6 +124,11 @@ struct AccountData {
     bool legacy = false;
     bool canMigrateToMSA = false;
 
+    QString customAuthServerUrl;
+    QString customAccountServerUrl;
+    QString customSessionServerUrl;
+    QString customServicesServerUrl;
+
     QString msaClientID;
     Katabasis::Token msaToken;
     Katabasis::Token userToken;
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index 9e2fd111..edbf2298 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -297,9 +297,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
                 return account->accountDisplayString();
 
             case TypeColumn: {
-                auto typeStr = account->typeString();
-                typeStr[0] = typeStr[0].toUpper();
-                return typeStr;
+                return account->typeDisplayName();
             }
 
             case StatusColumn: {
diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h
index a75df506..a7a9503b 100644
--- a/launcher/minecraft/auth/AuthSession.h
+++ b/launcher/minecraft/auth/AuthSession.h
@@ -26,6 +26,13 @@ struct AuthSession
         GoneOrMigrated
     } status = Undetermined;
 
+    // API URLs
+    QString auth_server_url;
+    QString account_server_url;
+    QString session_server_url;
+    QString services_server_url;
+    bool uses_custom_api_servers = false;
+
     // client token
     QString client_token;
     // account user name
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 3b050ac0..09a617b1 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -50,6 +50,7 @@
 
 #include "flows/MSA.h"
 #include "flows/Mojang.h"
+#include "flows/CustomYggdrasil.h"
 #include "flows/Offline.h"
 
 MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
@@ -82,6 +83,26 @@ MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username
     return account;
 }
 
+MinecraftAccountPtr MinecraftAccount::createFromUsernameCustomYggdrasil(
+    const QString &username,
+    const QString &customAuthServerUrl,
+    const QString &customAccountServerUrl,
+    const QString &customSessionServerUrl,
+    const QString &customServicesServerUrl
+)
+{
+    MinecraftAccountPtr account = new MinecraftAccount();
+    account->data.type = AccountType::CustomYggdrasil;
+    account->data.yggdrasilToken.extra["userName"] = username;
+    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+
+    account->data.customAuthServerUrl = customAuthServerUrl;
+    account->data.customAccountServerUrl = customAccountServerUrl;
+    account->data.customSessionServerUrl = customSessionServerUrl;
+    account->data.customServicesServerUrl = customServicesServerUrl;
+    return account;
+}
+
 MinecraftAccountPtr MinecraftAccount::createBlankMSA()
 {
     MinecraftAccountPtr account(new MinecraftAccount());
@@ -140,6 +161,17 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
     return m_currentTask;
 }
 
+shared_qobject_ptr<AccountTask> MinecraftAccount::loginCustomYggdrasil(QString password) {
+    Q_ASSERT(m_currentTask.get() == nullptr);
+
+    m_currentTask.reset(new CustomYggdrasilLogin(&data, password));
+    connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+    connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+    connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
+    emit activityChanged(true);
+    return m_currentTask;
+}
+
 shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() {
     Q_ASSERT(m_currentTask.get() == nullptr);
 
@@ -310,6 +342,13 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
     {
         session->session = "-";
     }
+
+    // API URLs
+    session->auth_server_url = data.authServerUrl();
+    session->account_server_url = data.accountServerUrl();
+    session->session_server_url = data.sessionServerUrl();
+    session->services_server_url = data.servicesServerUrl();
+    session->uses_custom_api_servers = data.usesCustomApiServers();
 }
 
 void MinecraftAccount::decrementUses()
diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index 0dcaeb53..61a5ab1d 100644
--- a/launcher/minecraft/auth/MinecraftAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -90,6 +90,13 @@ public: /* construction */
     explicit MinecraftAccount(QObject *parent = 0);
 
     static MinecraftAccountPtr createFromUsername(const QString &username);
+    static MinecraftAccountPtr createFromUsernameCustomYggdrasil(
+        const QString &username,
+        const QString &authServerUrl,
+        const QString &accountServerUrl,
+        const QString &sessionServerUrl,
+        const QString &servicesServerUrl
+    );
 
     static MinecraftAccountPtr createBlankMSA();
 
@@ -109,6 +116,8 @@ public: /* manipulation */
      */
     shared_qobject_ptr<AccountTask> login(QString password);
 
+    shared_qobject_ptr<AccountTask> loginCustomYggdrasil(QString password);
+
     shared_qobject_ptr<AccountTask> loginMSA();
 
     shared_qobject_ptr<AccountTask> loginOffline();
@@ -122,6 +131,22 @@ public: /* queries */
         return data.internalId;
     }
 
+    QString authServerUrl() const {
+        return data.authServerUrl();
+    }
+
+    QString accountServerUrl() const {
+        return data.accountServerUrl();
+    }
+
+    QString sessionServerUrl() const {
+        return data.sessionServerUrl();
+    }
+
+    QString servicesServerUrl() const {
+        return data.servicesServerUrl();
+    }
+
     QString accountDisplayString() const {
         return data.accountDisplayString();
     }
@@ -164,6 +189,34 @@ public: /* queries */
         return data.profileId().size() != 0;
     }
 
+    QString typeDisplayName() const {
+        switch(data.type) {
+            case AccountType::Mojang: {
+                if(data.legacy) {
+                    return "Legacy";
+                }
+                return "Mojang";
+            }
+            break;
+            case AccountType::CustomYggdrasil: {
+                return "Custom Yggdrasil";
+            }
+            break;
+            case AccountType::MSA: {
+                return "Microsoft";
+            }
+            break;
+            case AccountType::Offline: {
+                return "Offline";
+            }
+            break;
+            default: {
+                return "Unknown";
+            }
+
+        }
+    }
+
     QString typeString() const {
         switch(data.type) {
             case AccountType::Mojang: {
@@ -173,6 +226,13 @@ public: /* queries */
                 return "mojang";
             }
             break;
+            case AccountType::CustomYggdrasil: {
+                // This typeString gets passed to Minecraft; any Yggdrasil
+                // account should have the "mojang" type regardless of which
+                // servers are used.
+                return "mojang";
+            }
+            break;
             case AccountType::MSA: {
                 return "msa";
             }
diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp
index 29978411..f5ddf776 100644
--- a/launcher/minecraft/auth/Yggdrasil.cpp
+++ b/launcher/minecraft/auth/Yggdrasil.cpp
@@ -25,6 +25,7 @@
 
 #include <QDebug>
 
+#include "BuildConfig.h"
 #include "Application.h"
 
 Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
@@ -72,6 +73,8 @@ void Yggdrasil::refresh() {
     QJsonObject req;
     req.insert("clientToken", m_data->clientToken());
     req.insert("accessToken", m_data->accessToken());
+
+    qDebug() << "refreshing, access token is" << m_data->accessToken();
     /*
     {
         auto currentProfile = m_account->currentProfile();
@@ -84,7 +87,7 @@ void Yggdrasil::refresh() {
     req.insert("requestUser", false);
     QJsonDocument doc(req);
 
-    QUrl reqUrl("https://authserver.mojang.com/refresh");
+    QUrl reqUrl(m_data->authServerUrl() + "/refresh");
     QByteArray requestData = doc.toJson();
 
     sendRequest(reqUrl, requestData);
@@ -129,7 +132,7 @@ void Yggdrasil::login(QString password) {
 
     QJsonDocument doc(req);
 
-    QUrl reqUrl("https://authserver.mojang.com/authenticate");
+    QUrl reqUrl(m_data->authServerUrl() + "/authenticate");
     QNetworkRequest netRequest(reqUrl);
     QByteArray requestData = doc.toJson();
 
diff --git a/launcher/minecraft/auth/flows/CustomYggdrasil.cpp b/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
new file mode 100644
index 00000000..a8ee6f3d
--- /dev/null
+++ b/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
@@ -0,0 +1,25 @@
+#include "CustomYggdrasil.h"
+
+#include "minecraft/auth/steps/YggdrasilStep.h"
+#include "minecraft/auth/steps/MinecraftProfileStepMojang.h"
+#include "minecraft/auth/steps/MigrationEligibilityStep.h"
+#include "minecraft/auth/steps/GetSkinStep.h"
+
+CustomYggdrasilRefresh::CustomYggdrasilRefresh(
+    AccountData *data,
+    QObject *parent
+) : AuthFlow(data, parent) {
+    m_steps.append(new YggdrasilStep(m_data, QString()));
+    m_steps.append(new MinecraftProfileStepMojang(m_data));
+    m_steps.append(new GetSkinStep(m_data));
+}
+
+CustomYggdrasilLogin::CustomYggdrasilLogin(
+    AccountData *data,
+    QString password,
+    QObject *parent
+): AuthFlow(data, parent), m_password(password) {
+    m_steps.append(new YggdrasilStep(m_data, m_password));
+    m_steps.append(new MinecraftProfileStepMojang(m_data));
+    m_steps.append(new GetSkinStep(m_data));
+}
diff --git a/launcher/minecraft/auth/flows/CustomYggdrasil.h b/launcher/minecraft/auth/flows/CustomYggdrasil.h
new file mode 100644
index 00000000..cd82bec4
--- /dev/null
+++ b/launcher/minecraft/auth/flows/CustomYggdrasil.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "AuthFlow.h"
+
+class CustomYggdrasilRefresh : public AuthFlow
+{
+    Q_OBJECT
+public:
+    explicit CustomYggdrasilRefresh(
+        AccountData *data,
+        QObject *parent = 0
+    );
+};
+
+class CustomYggdrasilLogin : public AuthFlow
+{
+    Q_OBJECT
+public:
+    explicit CustomYggdrasilLogin(
+        AccountData *data,
+        QString password,
+        QObject *parent = 0
+    );
+
+private:
+    QString m_password;
+};
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
index 6cfa7c1c..16230e04 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -44,7 +44,7 @@ void MinecraftProfileStep::onRequestDone(
     qCDebug(authCredentials()) << data;
     if (error == QNetworkReply::ContentNotFoundError) {
         // NOTE: Succeed even if we do not have a profile. This is a valid account state.
-        if(m_data->type == AccountType::Mojang) {
+        if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
             m_data->minecraftEntitlement.canPlayMinecraft = false;
             m_data->minecraftEntitlement.ownsMinecraft = false;
         }
@@ -87,7 +87,7 @@ void MinecraftProfileStep::onRequestDone(
         return;
     }
 
-    if(m_data->type == AccountType::Mojang) {
+    if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
         auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
         m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
         m_data->minecraftEntitlement.ownsMinecraft = validProfile;
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
index 8c378588..f8d4baed 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
@@ -3,6 +3,7 @@
 #include <QNetworkRequest>
 
 #include "Logging.h"
+#include "BuildConfig.h"
 #include "minecraft/auth/AuthRequest.h"
 #include "minecraft/auth/Parsers.h"
 #include "net/NetUtils.h"
@@ -17,7 +18,6 @@ QString MinecraftProfileStepMojang::describe() {
     return tr("Fetching the Minecraft profile.");
 }
 
-
 void MinecraftProfileStepMojang::perform() {
     if (m_data->minecraftProfile.id.isEmpty()) {
         emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
@@ -25,7 +25,8 @@ void MinecraftProfileStepMojang::perform() {
     }
 
     // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts
-    QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id);
+
+    QUrl url = QUrl(m_data->sessionServerUrl() + "/session/minecraft/profile/" + m_data->minecraftProfile.id);
     QNetworkRequest req = QNetworkRequest(url);
     AuthRequest *request = new AuthRequest(this);
     connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone);
@@ -47,7 +48,7 @@ void MinecraftProfileStepMojang::onRequestDone(
     qCDebug(authCredentials()) << data;
     if (error == QNetworkReply::ContentNotFoundError) {
         // NOTE: Succeed even if we do not have a profile. This is a valid account state.
-        if(m_data->type == AccountType::Mojang) {
+        if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
             m_data->minecraftEntitlement.canPlayMinecraft = false;
             m_data->minecraftEntitlement.ownsMinecraft = false;
         }
@@ -90,7 +91,7 @@ void MinecraftProfileStepMojang::onRequestDone(
         return;
     }
 
-    if(m_data->type == AccountType::Mojang) {
+    if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
         auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
         m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
         m_data->minecraftEntitlement.ownsMinecraft = validProfile;
diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp
index ca55cd2e..a9a34872 100644
--- a/launcher/minecraft/launch/DirectJavaLaunch.cpp
+++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp
@@ -39,6 +39,8 @@ void DirectJavaLaunch::executeTask()
     std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
     QStringList args = minecraftInstance->javaArguments();
 
+    args.append(minecraftInstance->processAuthArgs(m_session));
+
     args.append("-Djava.library.path=" + minecraftInstance->getNativePath());
 
     auto classPathEntries = minecraftInstance->getClassPath();
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index 8ecf715d..79e8bcd7 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -110,6 +110,9 @@ void LauncherPartLaunch::executeTask()
 
     m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin);
     QStringList args = minecraftInstance->javaArguments();
+
+    args.append(minecraftInstance->processAuthArgs(m_session));
+
     QString allArgs = args.join(", ");
     emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher);
 
diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp
index 1d5ea36d..e6e70272 100644
--- a/launcher/minecraft/services/CapeChange.cpp
+++ b/launcher/minecraft/services/CapeChange.cpp
@@ -40,15 +40,16 @@
 
 #include "Application.h"
 
-CapeChange::CapeChange(QObject *parent, QString token, QString cape)
-    : Task(parent), m_capeId(cape), m_token(token)
+CapeChange::CapeChange(QObject *parent, MinecraftAccountPtr acct, QString cape)
+    : Task(parent), m_capeId(cape), m_acct(acct)
 {
 }
 
 void CapeChange::setCape(QString& cape) {
-    QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
+    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/capes/active"));
+    QString token = m_acct->accessToken();
     auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
-    request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
+    request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8());
 
     setStatus(tr("Equipping cape"));
@@ -65,9 +66,10 @@ void CapeChange::setCape(QString& cape) {
 }
 
 void CapeChange::clearCape() {
-    QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
+    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/capes/active"));
+    QString token = m_acct->accessToken();
     auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
-    request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
+    request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QNetworkReply *rep = APPLICATION->network()->deleteResource(request);
 
     setStatus(tr("Removing cape"));
diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h
index 38069f90..494b59e0 100644
--- a/launcher/minecraft/services/CapeChange.h
+++ b/launcher/minecraft/services/CapeChange.h
@@ -5,12 +5,13 @@
 #include <memory>
 #include "tasks/Task.h"
 #include "QObjectPtr.h"
+#include <minecraft/auth/MinecraftAccount.h>
 
 class CapeChange : public Task
 {
     Q_OBJECT
 public:
-    CapeChange(QObject *parent, QString token, QString capeId);
+    CapeChange(QObject *parent, MinecraftAccountPtr m_acct, QString capeId);
     virtual ~CapeChange() {}
 
 private:
@@ -19,7 +20,7 @@ private:
 
 private:
     QString m_capeId;
-    QString m_token;
+    MinecraftAccountPtr m_acct;
     shared_qobject_ptr<QNetworkReply> m_reply;
 
 protected:
diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp
index 711f8739..c7eaa51e 100644
--- a/launcher/minecraft/services/SkinUpload.cpp
+++ b/launcher/minecraft/services/SkinUpload.cpp
@@ -51,15 +51,16 @@ QByteArray getVariant(SkinUpload::Model model) {
     }
 }
 
-SkinUpload::SkinUpload(QObject *parent, QString token, QByteArray skin, SkinUpload::Model model)
-    : Task(parent), m_model(model), m_skin(skin), m_token(token)
+SkinUpload::SkinUpload(QObject *parent, MinecraftAccountPtr acct, QByteArray skin, SkinUpload::Model model)
+    : Task(parent), m_model(model), m_skin(skin), m_acct(acct)
 {
 }
 
 void SkinUpload::executeTask()
 {
-    QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
-    request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
+    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/skins"));
+    QString token = m_acct->accessToken();
+    request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
 
     QHttpPart skin;
diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h
index ac8c5b36..5d0592e0 100644
--- a/launcher/minecraft/services/SkinUpload.h
+++ b/launcher/minecraft/services/SkinUpload.h
@@ -4,6 +4,7 @@
 #include <QtNetwork/QtNetwork>
 #include <memory>
 #include "tasks/Task.h"
+#include <minecraft/auth/MinecraftAccount.h>
 
 typedef shared_qobject_ptr<class SkinUpload> SkinUploadPtr;
 
@@ -18,13 +19,13 @@ public:
     };
 
     // Note this class takes ownership of the file.
-    SkinUpload(QObject *parent, QString token, QByteArray skin, Model model = STEVE);
+    SkinUpload(QObject *parent, MinecraftAccountPtr acct, QByteArray skin, Model model = STEVE);
     virtual ~SkinUpload() {}
 
 private:
     Model m_model;
     QByteArray m_skin;
-    QString m_token;
+    MinecraftAccountPtr m_acct;
     shared_qobject_ptr<QNetworkReply> m_reply;
 protected:
     virtual void executeTask();
diff --git a/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp
new file mode 100644
index 00000000..bbbe278d
--- /dev/null
+++ b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp
@@ -0,0 +1,148 @@
+/* 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 "CustomYggdrasilLoginDialog.h"
+#include "ui_CustomYggdrasilLoginDialog.h"
+
+#include "minecraft/auth/AccountTask.h"
+
+#include <QtWidgets/QPushButton>
+
+CustomYggdrasilLoginDialog::CustomYggdrasilLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CustomYggdrasilLoginDialog)
+{
+    ui->setupUi(this);
+    ui->progressBar->setVisible(false);
+    ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+    connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+    connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+CustomYggdrasilLoginDialog::~CustomYggdrasilLoginDialog()
+{
+    delete ui;
+}
+
+QString CustomYggdrasilLoginDialog::fixUrl(QString url)
+{
+    QString fixed(url);
+    if (!fixed.contains("://")) {
+        fixed.prepend("https://");
+    }
+    if (fixed.endsWith("/")) {
+        fixed = fixed.left(fixed.size() - 1);
+    }
+    return fixed;
+}
+
+// Stage 1: User interaction
+void CustomYggdrasilLoginDialog::accept()
+{
+    setUserInputsEnabled(false);
+    ui->progressBar->setVisible(true);
+
+    // Setup the login task and start it
+    m_account = MinecraftAccount::createFromUsernameCustomYggdrasil(
+        ui->userTextBox->text(),
+        CustomYggdrasilLoginDialog::fixUrl(ui->authServerTextBox->text()),
+        CustomYggdrasilLoginDialog::fixUrl(ui->accountServerTextBox->text()),
+        CustomYggdrasilLoginDialog::fixUrl(ui->sessionServerTextBox->text()),
+        CustomYggdrasilLoginDialog::fixUrl(ui->servicesServerTextBox->text())
+    );
+
+    m_loginTask = m_account->loginCustomYggdrasil(ui->passTextBox->text());
+    connect(m_loginTask.get(), &Task::failed, this, &CustomYggdrasilLoginDialog::onTaskFailed);
+    connect(m_loginTask.get(), &Task::succeeded, this, &CustomYggdrasilLoginDialog::onTaskSucceeded);
+    connect(m_loginTask.get(), &Task::status, this, &CustomYggdrasilLoginDialog::onTaskStatus);
+    connect(m_loginTask.get(), &Task::progress, this, &CustomYggdrasilLoginDialog::onTaskProgress);
+    m_loginTask->start();
+}
+
+void CustomYggdrasilLoginDialog::setUserInputsEnabled(bool enable)
+{
+    ui->userTextBox->setEnabled(enable);
+    ui->passTextBox->setEnabled(enable);
+    ui->authServerTextBox->setEnabled(enable);
+    ui->accountServerTextBox->setEnabled(enable);
+    ui->sessionServerTextBox->setEnabled(enable);
+    ui->servicesServerTextBox->setEnabled(enable);
+    ui->buttonBox->setEnabled(enable);
+}
+
+// Enable the OK button only when all textboxes contain something.
+void CustomYggdrasilLoginDialog::on_userTextBox_textEdited(const QString &newText)
+{
+    ui->buttonBox->button(QDialogButtonBox::Ok)
+        ->setEnabled(!newText.isEmpty() &&
+                     !ui->passTextBox->text().isEmpty() &&
+                     !ui->authServerTextBox->text().isEmpty() &&
+                     !ui->accountServerTextBox->text().isEmpty() &&
+                     !ui->sessionServerTextBox->text().isEmpty() &&
+                     !ui->servicesServerTextBox->text().isEmpty());
+
+}
+void CustomYggdrasilLoginDialog::on_passTextBox_textEdited(const QString &newText)
+{
+    ui->buttonBox->button(QDialogButtonBox::Ok)
+        ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
+}
+
+void CustomYggdrasilLoginDialog::onTaskFailed(const QString &reason)
+{
+    // Set message
+    auto lines = reason.split('\n');
+    QString processed;
+    for(auto line: lines) {
+        if(line.size()) {
+            processed += "<font color='red'>" + line + "</font><br />";
+        }
+        else {
+            processed += "<br />";
+        }
+    }
+    ui->label->setText(processed);
+
+    // Re-enable user-interaction
+    setUserInputsEnabled(true);
+    ui->progressBar->setVisible(false);
+}
+
+void CustomYggdrasilLoginDialog::onTaskSucceeded()
+{
+    QDialog::accept();
+}
+
+void CustomYggdrasilLoginDialog::onTaskStatus(const QString &status)
+{
+    ui->label->setText(status);
+}
+
+void CustomYggdrasilLoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+    ui->progressBar->setMaximum(total);
+    ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr CustomYggdrasilLoginDialog::newAccount(QWidget *parent, QString msg)
+{
+    CustomYggdrasilLoginDialog dlg(parent);
+    dlg.ui->label->setText(msg);
+    if (dlg.exec() == QDialog::Accepted)
+    {
+        return dlg.m_account;
+    }
+    return nullptr;
+}
diff --git a/launcher/ui/dialogs/CustomYggdrasilLoginDialog.h b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.h
new file mode 100644
index 00000000..e2b23bd5
--- /dev/null
+++ b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.h
@@ -0,0 +1,60 @@
+/* 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"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class CustomYggdrasilLoginDialog;
+}
+
+class CustomYggdrasilLoginDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    ~CustomYggdrasilLoginDialog();
+
+    static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
+
+private:
+    explicit CustomYggdrasilLoginDialog(QWidget *parent = 0);
+
+    void setUserInputsEnabled(bool enable);
+
+protected
+slots:
+    void accept();
+
+    void onTaskFailed(const QString &reason);
+    void onTaskSucceeded();
+    void onTaskStatus(const QString &status);
+    void onTaskProgress(qint64 current, qint64 total);
+
+    void on_userTextBox_textEdited(const QString &newText);
+    void on_passTextBox_textEdited(const QString &newText);
+
+private:
+    QString fixUrl(QString url);
+    Ui::CustomYggdrasilLoginDialog *ui;
+    MinecraftAccountPtr m_account;
+    Task::Ptr m_loginTask;
+};
diff --git a/launcher/ui/dialogs/CustomYggdrasilLoginDialog.ui b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.ui
new file mode 100644
index 00000000..796aebcf
--- /dev/null
+++ b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.ui
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CustomYggdrasilLoginDialog</class>
+ <widget class="QDialog" name="CustomYggdrasilLoginDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>421</width>
+    <height>198</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Add 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="QLineEdit" name="userTextBox">
+     <property name="placeholderText">
+       <string>Email/username</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="passTextBox">
+     <property name="echoMode">
+      <enum>QLineEdit::Password</enum>
+     </property>
+     <property name="placeholderText">
+      <string>Password</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="authServerTextBox">
+     <property name="placeholderText">
+       <string>https://authserver.mojang.com</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="accountServerTextBox">
+     <property name="placeholderText">
+       <string>https://api.mojang.com</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="sessionServerTextBox">
+     <property name="placeholderText">
+       <string>https://sessionserver.mojang.com</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="servicesServerTextBox">
+     <property name="placeholderText">
+       <string>https://api.minecraftservices.com</string>
+     </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|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp
index 8180ac1f..30f14bb1 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.cpp
+++ b/launcher/ui/dialogs/SkinUploadDialog.cpp
@@ -120,12 +120,12 @@ void SkinUploadDialog::on_buttonBox_accepted()
         {
             model = SkinUpload::ALEX;
         }
-        skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
+        skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct, FS::read(fileName), model)));
     }
 
     auto selectedCape = ui->capeCombo->currentData().toString();
     if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
-        skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
+        skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct, selectedCape)));
     }
     if (prog.execWithTask(&skinUpload) != QDialog::Accepted)
     {
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index 278f45c4..432050ce 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -47,6 +47,7 @@
 #include "ui/dialogs/ProgressDialog.h"
 #include "ui/dialogs/OfflineLoginDialog.h"
 #include "ui/dialogs/LoginDialog.h"
+#include "ui/dialogs/CustomYggdrasilLoginDialog.h"
 #include "ui/dialogs/MSALoginDialog.h"
 #include "ui/dialogs/CustomMessageBox.h"
 #include "ui/dialogs/SkinUploadDialog.h"
@@ -157,6 +158,22 @@ void AccountListPage::on_actionAddMojang_triggered()
     }
 }
 
+void AccountListPage::on_actionAddCustomYggdrasil_triggered()
+{
+    MinecraftAccountPtr account = CustomYggdrasilLoginDialog::newAccount(
+        this,
+        tr("Please enter your email/username, password, and the URLs of your API servers.")
+    );
+
+    if (account)
+    {
+        m_accounts->addAccount(account);
+        if (m_accounts->count() == 1) {
+            m_accounts->setDefaultAccount(account);
+        }
+    }
+}
+
 void AccountListPage::on_actionAddMicrosoft_triggered()
 {
     if(BuildConfig.BUILD_PLATFORM == "osx64") {
diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h
index 9395e92b..3d163146 100644
--- a/launcher/ui/pages/global/AccountListPage.h
+++ b/launcher/ui/pages/global/AccountListPage.h
@@ -83,6 +83,7 @@ public:
 
 public slots:
     void on_actionAddMojang_triggered();
+    void on_actionAddCustomYggdrasil_triggered();
     void on_actionAddMicrosoft_triggered();
     void on_actionAddOffline_triggered();
     void on_actionRemove_triggered();
diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui
index 469955b5..a084a538 100644
--- a/launcher/ui/pages/global/AccountListPage.ui
+++ b/launcher/ui/pages/global/AccountListPage.ui
@@ -54,6 +54,7 @@
    </attribute>
    <addaction name="actionAddMicrosoft"/>
    <addaction name="actionAddMojang"/>
+   <addaction name="actionAddCustomYggdrasil"/>
    <addaction name="actionAddOffline"/>
    <addaction name="actionRefresh"/>
    <addaction name="actionRemove"/>
@@ -68,6 +69,11 @@
     <string>Add &amp;Mojang</string>
    </property>
   </action>
+  <action name="actionAddCustomYggdrasil">
+   <property name="text">
+    <string>Add &amp;Custom Yggdrasil</string>
+   </property>
+  </action>
   <action name="actionRemove">
    <property name="text">
     <string>Remo&amp;ve</string>
-- 
2.41.0


From 11fc89f0c76e7dbc20d8c7085cb328fb88702d27 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Thu, 5 Jan 2023 21:58:08 -0500
Subject: [PATCH 2/6] Allow multiple accounts with the same player UUID

Signed-off-by: Evan Goode <mail@evangoo.de>
---
 launcher/minecraft/auth/AccountList.cpp       | 22 +++++++++----------
 launcher/minecraft/auth/MinecraftAccount.cpp  |  2 +-
 launcher/minecraft/auth/MinecraftAccount.h    |  4 ++++
 .../minecraft/auth/flows/CustomYggdrasil.cpp  | 12 +++++-----
 4 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index edbf2298..6cbb6fb6 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -122,8 +122,17 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
 
     // override/replace existing account with the same profileId
     auto profileId = account->profileId();
-    if(profileId.size()) {
-        auto existingAccount = findAccountByProfileId(profileId);
+    if(profileId.size() && account->isMojangOrMSA()) {
+        int existingAccount = -1;
+        for (int i = 0; i < count(); i++) {
+            MinecraftAccountPtr existing = at(i);
+            if (existing->profileId() == profileId &&
+                existing->isMojangOrMSA()) {
+                existingAccount = i;
+                break;
+            }
+        }
+
         if(existingAccount != -1) {
             qDebug() << "Replacing old account with a new one with the same profile ID!";
 
@@ -525,9 +534,6 @@ bool AccountList::loadV2(QJsonObject& root) {
             if(!profileId.size()) {
                 continue;
             }
-            if(findAccountByProfileId(profileId) != -1) {
-                continue;
-            }
             connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
             connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
             m_accounts.append(account);
@@ -553,12 +559,6 @@ bool AccountList::loadV3(QJsonObject& root) {
         MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj);
         if (account.get() != nullptr)
         {
-            auto profileId = account->profileId();
-            if(profileId.size()) {
-                if(findAccountByProfileId(profileId) != -1) {
-                    continue;
-                }
-            }
             connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
             connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
             m_accounts.append(account);
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 09a617b1..d715532c 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -91,7 +91,7 @@ MinecraftAccountPtr MinecraftAccount::createFromUsernameCustomYggdrasil(
     const QString &customServicesServerUrl
 )
 {
-    MinecraftAccountPtr account = new MinecraftAccount();
+    auto account = makeShared<MinecraftAccount>();
     account->data.type = AccountType::CustomYggdrasil;
     account->data.yggdrasilToken.extra["userName"] = username;
     account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index 61a5ab1d..875549b8 100644
--- a/launcher/minecraft/auth/MinecraftAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -173,6 +173,10 @@ public: /* queries */
         return data.canMigrateToMSA;
     }
 
+    bool isMojangOrMSA() const {
+        return data.type == AccountType::Mojang || data.type == AccountType::MSA;
+    }
+
     bool isMSA() const {
         return data.type == AccountType::MSA;
     }
diff --git a/launcher/minecraft/auth/flows/CustomYggdrasil.cpp b/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
index a8ee6f3d..bda0eb2e 100644
--- a/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
+++ b/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
@@ -9,9 +9,9 @@ CustomYggdrasilRefresh::CustomYggdrasilRefresh(
     AccountData *data,
     QObject *parent
 ) : AuthFlow(data, parent) {
-    m_steps.append(new YggdrasilStep(m_data, QString()));
-    m_steps.append(new MinecraftProfileStepMojang(m_data));
-    m_steps.append(new GetSkinStep(m_data));
+    m_steps.append(makeShared<YggdrasilStep>(m_data, QString()));
+    m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
+    m_steps.append(makeShared<GetSkinStep>(m_data));
 }
 
 CustomYggdrasilLogin::CustomYggdrasilLogin(
@@ -19,7 +19,7 @@ CustomYggdrasilLogin::CustomYggdrasilLogin(
     QString password,
     QObject *parent
 ): AuthFlow(data, parent), m_password(password) {
-    m_steps.append(new YggdrasilStep(m_data, m_password));
-    m_steps.append(new MinecraftProfileStepMojang(m_data));
-    m_steps.append(new GetSkinStep(m_data));
+    m_steps.append(makeShared<YggdrasilStep>(m_data, m_password));
+    m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
+    m_steps.append(makeShared<GetSkinStep>(m_data));
 }
-- 
2.41.0


From d14b67f328dd50a8403c7d2454463f3159323556 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Tue, 16 May 2023 00:52:37 -0400
Subject: [PATCH 3/6] Use correct services server URL for SkinDelete

Signed-off-by: Evan Goode <mail@evangoo.de>
---
 launcher/minecraft/services/SkinDelete.cpp   | 9 +++++----
 launcher/minecraft/services/SkinDelete.h     | 5 +++--
 launcher/ui/pages/global/AccountListPage.cpp | 2 +-
 3 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp
index fbaaeacb..120c7a24 100644
--- a/launcher/minecraft/services/SkinDelete.cpp
+++ b/launcher/minecraft/services/SkinDelete.cpp
@@ -40,15 +40,16 @@
 
 #include "Application.h"
 
-SkinDelete::SkinDelete(QObject *parent, QString token)
-    : Task(parent), m_token(token)
+SkinDelete::SkinDelete(QObject *parent, MinecraftAccountPtr acct)
+    : Task(parent), m_acct(acct)
 {
 }
 
 void SkinDelete::executeTask()
 {
-    QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"));
-    request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
+    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/skins/active"));
+    QString token = m_acct->accessToken();
+    request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QNetworkReply *rep = APPLICATION->network()->deleteResource(request);
     m_reply = shared_qobject_ptr<QNetworkReply>(rep);
 
diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h
index b9a1c9d3..3948f712 100644
--- a/launcher/minecraft/services/SkinDelete.h
+++ b/launcher/minecraft/services/SkinDelete.h
@@ -3,6 +3,7 @@
 #include <QFile>
 #include <QtNetwork/QtNetwork>
 #include "tasks/Task.h"
+#include <minecraft/auth/MinecraftAccount.h>
 
 typedef shared_qobject_ptr<class SkinDelete> SkinDeletePtr;
 
@@ -10,11 +11,11 @@ class SkinDelete : public Task
 {
     Q_OBJECT
 public:
-    SkinDelete(QObject *parent, QString token);
+    SkinDelete(QObject *parent, MinecraftAccountPtr acct);
     virtual ~SkinDelete() = default;
 
 private:
-    QString m_token;
+    MinecraftAccountPtr m_acct;
     shared_qobject_ptr<QNetworkReply> m_reply;
 
 protected:
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index 432050ce..49de9bb3 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -332,7 +332,7 @@ void AccountListPage::on_actionDeleteSkin_triggered()
     QModelIndex selected = selection.first();
     MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
     ProgressDialog prog(this);
-    auto deleteSkinTask = std::make_shared<SkinDelete>(this, account->accessToken());
+    auto deleteSkinTask = std::make_shared<SkinDelete>(this, account);
     if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) {
         CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec();
         return;
-- 
2.41.0


From 8575571783b0e346c759b000cf8d7de98d6cd550 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Tue, 16 May 2023 01:18:19 -0400
Subject: [PATCH 4/6] Correctly use CustomYggdrasilRefresh, add warning message

Signed-off-by: Evan Goode <mail@evangoo.de>
---
 launcher/minecraft/auth/MinecraftAccount.cpp      | 3 +++
 launcher/minecraft/auth/flows/CustomYggdrasil.cpp | 1 -
 launcher/ui/pages/global/AccountListPage.cpp      | 6 +++++-
 3 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index d715532c..55ce2fd8 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -205,6 +205,9 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
     else if(data.type == AccountType::Offline) {
         m_currentTask.reset(new OfflineRefresh(&data));
     }
+    else if (data.type == AccountType::CustomYggdrasil) {
+        m_currentTask.reset(new CustomYggdrasilRefresh(&data));
+    }
     else {
         m_currentTask.reset(new MojangRefresh(&data));
     }
diff --git a/launcher/minecraft/auth/flows/CustomYggdrasil.cpp b/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
index bda0eb2e..5fbb5654 100644
--- a/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
+++ b/launcher/minecraft/auth/flows/CustomYggdrasil.cpp
@@ -2,7 +2,6 @@
 
 #include "minecraft/auth/steps/YggdrasilStep.h"
 #include "minecraft/auth/steps/MinecraftProfileStepMojang.h"
-#include "minecraft/auth/steps/MigrationEligibilityStep.h"
 #include "minecraft/auth/steps/GetSkinStep.h"
 
 CustomYggdrasilRefresh::CustomYggdrasilRefresh(
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index 49de9bb3..bb04f127 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -175,7 +175,11 @@ void AccountListPage::on_actionAddCustomYggdrasil_triggered()
 
     MinecraftAccountPtr account = CustomYggdrasilLoginDialog::newAccount(
         this,
-        tr("Please enter your email/username, password, and the URLs of your API servers.")
+        tr(
+            "Please enter your username (sometimes an email address), password, and the URLs of your API servers."
+            "<br><br>"
+            "<b>Caution!</b> Your username and password will be sent to the authentication server you specify!"
+        )
     );
 
     if (account)
-- 
2.41.0


From fdbb8b63717bc2a7e6bb608b0d1301c1e94005a0 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Thu, 15 Jun 2023 16:20:29 -0400
Subject: [PATCH 5/6] Custom Yggdrasil: Readability fixes

Also made the MinecraftEntitlement for Custom Yggdrasil accounts work
just like offline accounts---instead of checking the reply from the auth
server, Custom Yggdrasil accounts are granted canPlayMinecraft and
ownsMinecraft when they are created, in MinecraftAccount.cpp.

Signed-off-by: Evan Goode <mail@evangoo.de>
---
 launcher/minecraft/auth/AccountData.cpp       |  4 +--
 launcher/minecraft/auth/AccountList.cpp       | 25 ++++++++-----------
 launcher/minecraft/auth/MinecraftAccount.cpp  | 17 +++++++------
 launcher/minecraft/auth/Yggdrasil.cpp         |  2 --
 .../auth/steps/MinecraftProfileStep.cpp       |  4 +--
 .../auth/steps/MinecraftProfileStepMojang.cpp |  4 +--
 launcher/minecraft/services/CapeChange.cpp    | 10 ++++----
 launcher/minecraft/services/CapeChange.h      |  4 +--
 launcher/minecraft/services/SkinDelete.cpp    |  6 ++---
 launcher/minecraft/services/SkinDelete.h      |  2 +-
 launcher/minecraft/services/SkinUpload.cpp    |  6 ++---
 launcher/minecraft/services/SkinUpload.h      |  2 +-
 launcher/ui/dialogs/SkinUploadDialog.cpp      |  8 +++---
 launcher/ui/dialogs/SkinUploadDialog.h        |  2 +-
 14 files changed, 44 insertions(+), 52 deletions(-)

diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp
index 1f32efd4..3235c79c 100644
--- a/launcher/minecraft/auth/AccountData.cpp
+++ b/launcher/minecraft/auth/AccountData.cpp
@@ -524,9 +524,7 @@ QString AccountData::profileName() const {
 
 QString AccountData::accountDisplayString() const {
     switch(type) {
-        case AccountType::Mojang: {
-            return userName();
-        }
+        case AccountType::Mojang:
         case AccountType::CustomYggdrasil: {
             return userName();
         }
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index 6cbb6fb6..ecd27ba4 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -123,27 +123,22 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
     // override/replace existing account with the same profileId
     auto profileId = account->profileId();
     if(profileId.size() && account->isMojangOrMSA()) {
-        int existingAccount = -1;
-        for (int i = 0; i < count(); i++) {
-            MinecraftAccountPtr existing = at(i);
-            if (existing->profileId() == profileId &&
-                existing->isMojangOrMSA()) {
-                existingAccount = i;
-                break;
-            }
-        }
+        auto iter = std::find_if(m_accounts.constBegin(), m_accounts.constEnd(), [&](const MinecraftAccountPtr & existing) {
+            return existing->profileId() == profileId && existing->isMojangOrMSA();
+        });
 
-        if(existingAccount != -1) {
+        if(iter != m_accounts.constEnd()) {
             qDebug() << "Replacing old account with a new one with the same profile ID!";
 
-            MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount];
-            m_accounts[existingAccount] = account;
-            if(m_defaultAccount == existingAccountPtr) {
+            MinecraftAccountPtr existingAccount = *iter;
+            const auto existingAccountIndex = std::distance(m_accounts.constBegin(), iter);
+            m_accounts[existingAccountIndex] = account;
+            if(m_defaultAccount == existingAccount) {
                 m_defaultAccount = account;
             }
             // disconnect notifications for changes in the account being replaced
-            existingAccountPtr->disconnect(this);
-            emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1));
+            existingAccount->disconnect(this);
+            emit dataChanged(index(existingAccountIndex), index(existingAccountIndex, columnCount(QModelIndex()) - 1));
             onListChanged();
             return;
         }
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 55ce2fd8..26c86517 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -40,7 +40,6 @@
 #include <QUuid>
 #include <QJsonObject>
 #include <QJsonArray>
-#include <QRegularExpression>
 #include <QStringList>
 #include <QJsonDocument>
 
@@ -54,7 +53,7 @@
 #include "flows/Offline.h"
 
 MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
-    data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+    data.internalId = QUuid::createUuid().toString(QUuid::Id128);
 }
 
 
@@ -79,7 +78,7 @@ MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username
     auto account = makeShared<MinecraftAccount>();
     account->data.type = AccountType::Mojang;
     account->data.yggdrasilToken.extra["userName"] = username;
-    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128);
     return account;
 }
 
@@ -94,7 +93,9 @@ MinecraftAccountPtr MinecraftAccount::createFromUsernameCustomYggdrasil(
     auto account = makeShared<MinecraftAccount>();
     account->data.type = AccountType::CustomYggdrasil;
     account->data.yggdrasilToken.extra["userName"] = username;
-    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128);
+    account->data.minecraftEntitlement.ownsMinecraft = true;
+    account->data.minecraftEntitlement.canPlayMinecraft = true;
 
     account->data.customAuthServerUrl = customAuthServerUrl;
     account->data.customAccountServerUrl = customAccountServerUrl;
@@ -118,10 +119,10 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
     account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
     account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
     account->data.yggdrasilToken.extra["userName"] = username;
-    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128);
     account->data.minecraftEntitlement.ownsMinecraft = true;
     account->data.minecraftEntitlement.canPlayMinecraft = true;
-    account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+    account->data.minecraftProfile.id = QUuid::createUuid().toString(QUuid::Id128);
     account->data.minecraftProfile.name = username;
     account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
     return account;
@@ -165,8 +166,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginCustomYggdrasil(QString p
     Q_ASSERT(m_currentTask.get() == nullptr);
 
     m_currentTask.reset(new CustomYggdrasilLogin(&data, password));
-    connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
-    connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+    connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
+    connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
     connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
     emit activityChanged(true);
     return m_currentTask;
diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp
index f5ddf776..43fda881 100644
--- a/launcher/minecraft/auth/Yggdrasil.cpp
+++ b/launcher/minecraft/auth/Yggdrasil.cpp
@@ -73,8 +73,6 @@ void Yggdrasil::refresh() {
     QJsonObject req;
     req.insert("clientToken", m_data->clientToken());
     req.insert("accessToken", m_data->accessToken());
-
-    qDebug() << "refreshing, access token is" << m_data->accessToken();
     /*
     {
         auto currentProfile = m_account->currentProfile();
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
index 16230e04..6cfa7c1c 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -44,7 +44,7 @@ void MinecraftProfileStep::onRequestDone(
     qCDebug(authCredentials()) << data;
     if (error == QNetworkReply::ContentNotFoundError) {
         // NOTE: Succeed even if we do not have a profile. This is a valid account state.
-        if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
+        if(m_data->type == AccountType::Mojang) {
             m_data->minecraftEntitlement.canPlayMinecraft = false;
             m_data->minecraftEntitlement.ownsMinecraft = false;
         }
@@ -87,7 +87,7 @@ void MinecraftProfileStep::onRequestDone(
         return;
     }
 
-    if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
+    if(m_data->type == AccountType::Mojang) {
         auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
         m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
         m_data->minecraftEntitlement.ownsMinecraft = validProfile;
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
index f8d4baed..08174ece 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
@@ -48,7 +48,7 @@ void MinecraftProfileStepMojang::onRequestDone(
     qCDebug(authCredentials()) << data;
     if (error == QNetworkReply::ContentNotFoundError) {
         // NOTE: Succeed even if we do not have a profile. This is a valid account state.
-        if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
+        if(m_data->type == AccountType::Mojang) {
             m_data->minecraftEntitlement.canPlayMinecraft = false;
             m_data->minecraftEntitlement.ownsMinecraft = false;
         }
@@ -91,7 +91,7 @@ void MinecraftProfileStepMojang::onRequestDone(
         return;
     }
 
-    if(m_data->type == AccountType::Mojang || m_data->type == AccountType::CustomYggdrasil) {
+    if(m_data->type == AccountType::Mojang) {
         auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
         m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
         m_data->minecraftEntitlement.ownsMinecraft = validProfile;
diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp
index e6e70272..09a57863 100644
--- a/launcher/minecraft/services/CapeChange.cpp
+++ b/launcher/minecraft/services/CapeChange.cpp
@@ -41,13 +41,13 @@
 #include "Application.h"
 
 CapeChange::CapeChange(QObject *parent, MinecraftAccountPtr acct, QString cape)
-    : Task(parent), m_capeId(cape), m_acct(acct)
+    : Task(parent), m_capeId(cape), m_account(acct)
 {
 }
 
 void CapeChange::setCape(QString& cape) {
-    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/capes/active"));
-    QString token = m_acct->accessToken();
+    QNetworkRequest request(QUrl(m_account->servicesServerUrl() + "/minecraft/profile/capes/active"));
+    QString token = m_account->accessToken();
     auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
     request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8());
@@ -66,8 +66,8 @@ void CapeChange::setCape(QString& cape) {
 }
 
 void CapeChange::clearCape() {
-    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/capes/active"));
-    QString token = m_acct->accessToken();
+    QNetworkRequest request(QUrl(m_account->servicesServerUrl() + "/minecraft/profile/capes/active"));
+    QString token = m_account->accessToken();
     auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
     request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QNetworkReply *rep = APPLICATION->network()->deleteResource(request);
diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h
index 494b59e0..8f94d89b 100644
--- a/launcher/minecraft/services/CapeChange.h
+++ b/launcher/minecraft/services/CapeChange.h
@@ -11,7 +11,7 @@ class CapeChange : public Task
 {
     Q_OBJECT
 public:
-    CapeChange(QObject *parent, MinecraftAccountPtr m_acct, QString capeId);
+    CapeChange(QObject *parent, MinecraftAccountPtr m_account, QString capeId);
     virtual ~CapeChange() {}
 
 private:
@@ -20,7 +20,7 @@ private:
 
 private:
     QString m_capeId;
-    MinecraftAccountPtr m_acct;
+    MinecraftAccountPtr m_account;
     shared_qobject_ptr<QNetworkReply> m_reply;
 
 protected:
diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp
index 120c7a24..a350d15f 100644
--- a/launcher/minecraft/services/SkinDelete.cpp
+++ b/launcher/minecraft/services/SkinDelete.cpp
@@ -41,14 +41,14 @@
 #include "Application.h"
 
 SkinDelete::SkinDelete(QObject *parent, MinecraftAccountPtr acct)
-    : Task(parent), m_acct(acct)
+    : Task(parent), m_account(acct)
 {
 }
 
 void SkinDelete::executeTask()
 {
-    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/skins/active"));
-    QString token = m_acct->accessToken();
+    QNetworkRequest request(QUrl(m_account->servicesServerUrl() + "/minecraft/profile/skins/active"));
+    QString token = m_account->accessToken();
     request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QNetworkReply *rep = APPLICATION->network()->deleteResource(request);
     m_reply = shared_qobject_ptr<QNetworkReply>(rep);
diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h
index 3948f712..ee5a4b97 100644
--- a/launcher/minecraft/services/SkinDelete.h
+++ b/launcher/minecraft/services/SkinDelete.h
@@ -15,7 +15,7 @@ public:
     virtual ~SkinDelete() = default;
 
 private:
-    MinecraftAccountPtr m_acct;
+    MinecraftAccountPtr m_account;
     shared_qobject_ptr<QNetworkReply> m_reply;
 
 protected:
diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp
index c7eaa51e..b19f231c 100644
--- a/launcher/minecraft/services/SkinUpload.cpp
+++ b/launcher/minecraft/services/SkinUpload.cpp
@@ -52,14 +52,14 @@ QByteArray getVariant(SkinUpload::Model model) {
 }
 
 SkinUpload::SkinUpload(QObject *parent, MinecraftAccountPtr acct, QByteArray skin, SkinUpload::Model model)
-    : Task(parent), m_model(model), m_skin(skin), m_acct(acct)
+    : Task(parent), m_model(model), m_skin(skin), m_account(acct)
 {
 }
 
 void SkinUpload::executeTask()
 {
-    QNetworkRequest request(QUrl(m_acct->servicesServerUrl() + "/minecraft/profile/skins"));
-    QString token = m_acct->accessToken();
+    QNetworkRequest request(QUrl(m_account->servicesServerUrl() + "/minecraft/profile/skins"));
+    QString token = m_account->accessToken();
     request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toLocal8Bit());
     QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
 
diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h
index 5d0592e0..61cae484 100644
--- a/launcher/minecraft/services/SkinUpload.h
+++ b/launcher/minecraft/services/SkinUpload.h
@@ -25,7 +25,7 @@ public:
 private:
     Model m_model;
     QByteArray m_skin;
-    MinecraftAccountPtr m_acct;
+    MinecraftAccountPtr m_account;
     shared_qobject_ptr<QNetworkReply> m_reply;
 protected:
     virtual void executeTask();
diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp
index 30f14bb1..43459833 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.cpp
+++ b/launcher/ui/dialogs/SkinUploadDialog.cpp
@@ -120,12 +120,12 @@ void SkinUploadDialog::on_buttonBox_accepted()
         {
             model = SkinUpload::ALEX;
         }
-        skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct, FS::read(fileName), model)));
+        skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_account, FS::read(fileName), model)));
     }
 
     auto selectedCape = ui->capeCombo->currentData().toString();
-    if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
-        skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct, selectedCape)));
+    if(selectedCape != m_account->accountData()->minecraftProfile.currentCape) {
+        skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_account, selectedCape)));
     }
     if (prog.execWithTask(&skinUpload) != QDialog::Accepted)
     {
@@ -150,7 +150,7 @@ void SkinUploadDialog::on_skinBrowseBtn_clicked()
 }
 
 SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent)
-    :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
+    :QDialog(parent), m_account(acct), ui(new Ui::SkinUploadDialog)
 {
     ui->setupUi(this);
 
diff --git a/launcher/ui/dialogs/SkinUploadDialog.h b/launcher/ui/dialogs/SkinUploadDialog.h
index 84d17dc6..4f4a7188 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.h
+++ b/launcher/ui/dialogs/SkinUploadDialog.h
@@ -22,7 +22,7 @@ public slots:
     void on_skinBrowseBtn_clicked();
 
 protected:
-    MinecraftAccountPtr m_acct;
+    MinecraftAccountPtr m_account;
 
 private:
     Ui::SkinUploadDialog *ui;
-- 
2.41.0


From 242c1e43f9c6b423a77d41be30c19df2880dcaaf Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Sat, 24 Jun 2023 14:15:14 -0400
Subject: [PATCH 6/6] Custom Yggdrasil: Add extra confirmation dialog

Signed-off-by: Evan Goode <mail@evangoo.de>
---
 .../ui/dialogs/CustomYggdrasilLoginDialog.cpp   | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp
index bbbe278d..9f705ac1 100644
--- a/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp
+++ b/launcher/ui/dialogs/CustomYggdrasilLoginDialog.cpp
@@ -15,6 +15,7 @@
 
 #include "CustomYggdrasilLoginDialog.h"
 #include "ui_CustomYggdrasilLoginDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
 
 #include "minecraft/auth/AccountTask.h"
 
@@ -50,13 +51,27 @@ QString CustomYggdrasilLoginDialog::fixUrl(QString url)
 // Stage 1: User interaction
 void CustomYggdrasilLoginDialog::accept()
 {
+    auto fixedAuthUrl = CustomYggdrasilLoginDialog::fixUrl(ui->authServerTextBox->text());
+
+    auto response = CustomMessageBox::selectable(this, QObject::tr("Confirm account creation"),
+        QObject::tr(
+            "Warning: you are about to send the username and password you entered to an "
+            "unofficial, third-party authentication server:\n"
+            "%1\n\n"
+            "Never use your Mojang or Microsoft password for a third-party account!\n\n"
+            "Are you sure you want to proceed?"
+        ).arg(fixedAuthUrl),
+        QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)->exec();
+    if (response != QMessageBox::Yes)
+        return;
+
     setUserInputsEnabled(false);
     ui->progressBar->setVisible(true);
 
     // Setup the login task and start it
     m_account = MinecraftAccount::createFromUsernameCustomYggdrasil(
         ui->userTextBox->text(),
-        CustomYggdrasilLoginDialog::fixUrl(ui->authServerTextBox->text()),
+        fixedAuthUrl,
         CustomYggdrasilLoginDialog::fixUrl(ui->accountServerTextBox->text()),
         CustomYggdrasilLoginDialog::fixUrl(ui->sessionServerTextBox->text()),
         CustomYggdrasilLoginDialog::fixUrl(ui->servicesServerTextBox->text())
-- 
2.41.0