GH-3392 dirty initial MSA support that shares logic with Mojang flows

Both act as the first step of AuthContext.
This commit is contained in:
Petr Mrázek
2021-07-26 21:44:11 +02:00
parent fca2e9e44c
commit 3a53349e33
66 changed files with 2342 additions and 2477 deletions

View File

@ -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";
}
}
}

View File

@ -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;
};

View File

@ -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();
case TypeColumn: {
auto typeStr = account->typeString();
typeStr[0] = typeStr[0].toUpper();
return typeStr;
}
case ProfileNameColumn: {
return account->profileName();
}
default:
return QVariant();
}
case Qt::ToolTipRole:
return account->accountDisplayString();
case PointerRole:
return qVariantFromValue(account);
case Qt::CheckStateRole:
switch (index.column())
{
case NameColumn:
return account == m_activeAccount ? Qt::Checked : Qt::Unchecked;
}
default:
return QVariant();
}
case Qt::ToolTipRole:
return account->username();
case PointerRole:
return qVariantFromValue(account);
case Qt::CheckStateRole:
switch (index.column())
{
case ActiveColumn:
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)
bool AccountList::loadList()
{
beginResetModel();
m_accounts = versions;
endResetModel();
}
bool MojangAccountList::loadList(const QString &filePath)
{
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;
// Attempt to rename the old version.
file.rename(newName);
return false;
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;
}
}
}
// Now, load the accounts array.
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();
MojangAccountPtr account = MojangAccount::loadFromJson(accountObj);
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(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 (activeUserName.size() && account->mojangUserName() == activeUserName) {
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::loadV3(QJsonObject& root) {
beginResetModel();
QJsonArray accounts = root.value("accounts").toArray();
for (QJsonValue accountVal : accounts)
{
QJsonObject accountObj = accountVal.toObject();
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(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(accountObj.value("active").toBool(false)) {
m_activeAccount = account;
}
}
else
{
qWarning() << "Failed to load an account.";
}
}
endResetModel();
return true;
}
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)
{

View File

@ -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;
};

View File

@ -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);
}
}

View File

@ -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,46 +73,18 @@ 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;
} m_accountState = STATE_CREATED;
State accountState() {
return m_accountState;
}
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;
/**
* 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;
/**
* 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);
/**
* Returns the state message for the given state.
* Used to set the status message for the task.
@ -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;
};

View File

@ -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);

View File

@ -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;

View File

@ -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.";
}
}

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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);
};

View File

@ -0,0 +1,752 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QDesktopServices>
#include <QMetaEnum>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QUrlQuery>
#include <QPixmap>
#include <QPainter>
#include "AuthContext.h"
#include "katabasis/Globals.h"
#include "katabasis/Requestor.h"
#include "BuildConfig.h"
using OAuth2 = Katabasis::OAuth2;
using Requestor = Katabasis::Requestor;
using Activity = Katabasis::Activity;
AuthContext::AuthContext(AccountData * data, QObject *parent) :
AccountTask(data, parent)
{
mgr = new QNetworkAccessManager(this);
}
void AuthContext::beginActivity(Activity activity) {
if(isBusy()) {
throw 0;
}
m_activity = activity;
changeState(STATE_WORKING, "Initializing");
emit activityChanged(m_activity);
}
void AuthContext::finishActivity() {
if(!isBusy()) {
throw 0;
}
m_activity = Katabasis::Activity::Idle;
m_stage = MSAStage::Idle;
m_data->validity_ = m_data->minecraftProfile.validity;
emit activityChanged(m_activity);
}
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};
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr);
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);
}
void AuthContext::initMojang() {
if(m_yggdrasil) {
return;
}
m_yggdrasil = new Yggdrasil(m_data, this);
connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
}
void AuthContext::onMojangSucceeded() {
doMinecraftProfile();
}
void AuthContext::onMojangFailed() {
finishActivity();
m_error = m_yggdrasil->m_error;
m_aborted = m_yggdrasil->m_aborted;
changeState(m_yggdrasil->accountState(), "Microsoft user authentication failed.");
}
/*
bool AuthContext::signOut() {
if(isBusy()) {
return false;
}
start();
beginActivity(Activity::LoggingOut);
m_oauth2->unlink();
m_account = AccountData();
finishActivity();
return true;
}
*/
void AuthContext::onOpenBrowser(const QUrl &url) {
QDesktopServices::openUrl(url);
}
void AuthContext::onCloseBrowser() {
}
void AuthContext::onOAuthLinkingFailed() {
finishActivity();
changeState(STATE_FAILED_HARD, "Microsoft user authentication failed.");
}
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();
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
doUserAuth();
}
void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
// respond to activity change here
}
void AuthContext::doUserAuth() {
m_stage = MSAStage::UserAuth;
changeState(STATE_WORKING, "Starting user authentication");
QString xbox_auth_template = R"XXX(
{
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "d=%1"
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
auto *requestor = new Katabasis::Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "First layer of XBox auth ... commencing.";
}
namespace {
bool getDateTime(QJsonValue value, QDateTime & out) {
if(!value.isString()) {
return false;
}
out = QDateTime::fromString(value.toString(), Qt::ISODate);
return out.isValid();
}
bool getString(QJsonValue value, QString & out) {
if(!value.isString()) {
return false;
}
out = value.toString();
return true;
}
bool getNumber(QJsonValue value, double & out) {
if(!value.isDouble()) {
return false;
}
out = value.toDouble();
return true;
}
/*
{
"IssueInstant":"2020-12-07T19:52:08.4463796Z",
"NotAfter":"2020-12-21T19:52:08.4463796Z",
"Token":"token",
"DisplayClaims":{
"xui":[
{
"uhs":"userhash"
}
]
}
}
*/
// TODO: handle error responses ...
/*
{
"Identity":"0",
"XErr":2148916238,
"Message":"",
"Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
}
// 2148916233 = missing XBox account
// 2148916238 = child account not linked to a family
*/
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) {
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
qDebug() << data;
return false;
}
auto obj = doc.object();
if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) {
qWarning() << "User IssueInstant is not a timestamp";
qDebug() << data;
return false;
}
if(!getDateTime(obj.value("NotAfter"), output.notAfter)) {
qWarning() << "User NotAfter is not a timestamp";
qDebug() << data;
return false;
}
if(!getString(obj.value("Token"), output.token)) {
qWarning() << "User Token is not a timestamp";
qDebug() << data;
return false;
}
auto arrayVal = obj.value("DisplayClaims").toObject().value("xui");
if(!arrayVal.isArray()) {
qWarning() << "Missing xui claims array";
qDebug() << data;
return false;
}
bool foundUHS = false;
for(auto item: arrayVal.toArray()) {
if(!item.isObject()) {
continue;
}
auto obj = item.toObject();
if(obj.contains("uhs")) {
foundUHS = true;
} else {
continue;
}
// consume all 'display claims' ... whatever that means
for(auto iter = obj.begin(); iter != obj.end(); iter++) {
QString claim;
if(!getString(obj.value(iter.key()), claim)) {
qWarning() << "display claim " << iter.key() << " is not a string...";
qDebug() << data;
return false;
}
output.extra[iter.key()] = claim;
}
break;
}
if(!foundUHS) {
qWarning() << "Missing uhs";
qDebug() << data;
return false;
}
output.validity = Katabasis::Validity::Certain;
qDebug() << data;
return true;
}
}
void AuthContext::onUserAuthDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
finishActivity();
changeState(STATE_FAILED_HARD, "XBox user authentication failed.");
return;
}
Katabasis::Token temp;
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_data->userToken = temp;
m_stage = MSAStage::XboxAuth;
changeState(STATE_WORKING, "Starting XBox authentication");
doSTSAuthMinecraft();
doSTSAuthGeneric();
}
/*
url = "https://xsts.auth.xboxlive.com/xsts/authorize"
headers = {"x-xbl-contract-version": "1"}
data = {
"RelyingParty": relying_party,
"TokenType": "JWT",
"Properties": {
"UserTokens": [self.user_token.token],
"SandboxId": "RETAIL",
},
}
*/
void AuthContext::doSTSAuthMinecraft() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
"%1"
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Second layer of XBox auth ... commencing.";
}
void AuthContext::onSTSAuthMinecraftDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
m_requestsDone ++;
return;
}
Katabasis::Token temp;
if(!parseXTokenResponse(replyData, temp)) {
qWarning() << "Could not parse authorization response for access to mojang services...";
m_requestsDone ++;
return;
}
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
qDebug() << replyData;
m_requestsDone ++;
return;
}
m_data->mojangservicesToken = temp;
doMinecraftAuth();
}
void AuthContext::doSTSAuthGeneric() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
"%1"
]
},
"RelyingParty": "http://xboxlive.com",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Second layer of XBox auth ... commencing.";
}
void AuthContext::onSTSAuthGenericDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
m_requestsDone ++;
return;
}
Katabasis::Token temp;
if(!parseXTokenResponse(replyData, temp)) {
qWarning() << "Could not parse authorization response for access to xbox API...";
m_requestsDone ++;
return;
}
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
qDebug() << replyData;
m_requestsDone ++;
return;
}
m_data->xboxApiToken = temp;
doXBoxProfile();
}
void AuthContext::doMinecraftAuth() {
QString mc_auth_template = R"XXX(
{
"identityToken": "XBL3.0 x=%1;%2"
}
)XXX";
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, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone);
requestor->post(request, data.toUtf8());
qDebug() << "Getting Minecraft access token...";
}
namespace {
bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
qDebug() << data;
return false;
}
auto obj = doc.object();
double expires_in = 0;
if(!getNumber(obj.value("expires_in"), expires_in)) {
qWarning() << "expires_in is not a valid number";
qDebug() << data;
return false;
}
auto currentTime = QDateTime::currentDateTimeUtc();
output.issueInstant = currentTime;
output.notAfter = currentTime.addSecs(expires_in);
QString username;
if(!getString(obj.value("username"), username)) {
qWarning() << "username is not valid";
qDebug() << data;
return false;
}
// TODO: it's a JWT... validate it?
if(!getString(obj.value("access_token"), output.token)) {
qWarning() << "access_token is not valid";
qDebug() << data;
return false;
}
output.validity = Katabasis::Validity::Certain;
qDebug() << data;
return true;
}
}
void AuthContext::onMinecraftAuthDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
m_requestsDone ++;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qDebug() << replyData;
return;
}
if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response...";
qDebug() << replyData;
return;
}
m_mcAuthSucceeded = true;
checkResult();
}
void AuthContext::doXBoxProfile() {
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
QUrlQuery q;
q.addQueryItem(
"settings",
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
"PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
"UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
"PreferredColor,Location,Bio,Watermarks,"
"RealName,RealNameOverride,IsQuarantined"
);
url.setQuery(q);
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("x-xbl-contract-version", "3");
request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone);
requestor->get(request);
qDebug() << "Getting Xbox profile...";
}
void AuthContext::onXBoxProfileDone(
int requestId,
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
m_requestsDone ++;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qDebug() << replyData;
return;
}
qDebug() << "XBox profile: " << replyData;
m_xboxProfileSucceeded = true;
checkResult();
}
void AuthContext::checkResult() {
if(m_requestsDone != 2) {
return;
}
if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
doMinecraftProfile();
}
else {
finishActivity();
changeState(STATE_FAILED_HARD, "XBox and/or Mojang authentication steps did not succeed");
}
}
namespace {
bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
qDebug() << data;
return false;
}
auto obj = doc.object();
if(!getString(obj.value("id"), output.id)) {
qWarning() << "minecraft profile id is not a string";
qDebug() << data;
return false;
}
if(!getString(obj.value("name"), output.name)) {
qWarning() << "minecraft profile name is not a string";
qDebug() << data;
return false;
}
auto skinsArray = obj.value("skins").toArray();
for(auto skin: skinsArray) {
auto skinObj = skin.toObject();
Skin skinOut;
if(!getString(skinObj.value("id"), skinOut.id)) {
continue;
}
QString state;
if(!getString(skinObj.value("state"), state)) {
continue;
}
if(state != "ACTIVE") {
continue;
}
if(!getString(skinObj.value("url"), skinOut.url)) {
continue;
}
if(!getString(skinObj.value("variant"), skinOut.variant)) {
continue;
}
// we deal with only the active skin
output.skin = skinOut;
break;
}
auto capesArray = obj.value("capes").toArray();
int i = -1;
int currentCape = -1;
for(auto cape: capesArray) {
i++;
auto capeObj = cape.toObject();
Cape capeOut;
if(!getString(capeObj.value("id"), capeOut.id)) {
continue;
}
QString state;
if(!getString(capeObj.value("state"), state)) {
continue;
}
if(state == "ACTIVE") {
currentCape = i;
}
if(!getString(capeObj.value("url"), capeOut.url)) {
continue;
}
if(!getString(capeObj.value("alias"), capeOut.alias)) {
continue;
}
// we deal with only the active skin
output.capes.push_back(capeOut);
}
output.currentCape = currentCape;
output.validity = Katabasis::Validity::Certain;
return true;
}
}
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_data->yggdrasilToken.token).toUtf8());
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone);
requestor->get(request);
}
void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
qDebug() << data;
if (error == QNetworkReply::ContentNotFoundError) {
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_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
finishActivity();
changeState(STATE_FAILED_HARD, "Profile response could not be parsed");
return;
}
doGetSkin();
}
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, m_oauth2, this);
requestor->setAddAccessTokenInQuery(false);
connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone);
requestor->get(request);
}
void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
if (error == QNetworkReply::NoError) {
m_data->minecraftProfile.skin.data = data;
}
m_data->validity_ = Katabasis::Validity::Certain;
finishActivity();
changeState(STATE_SUCCEEDED, "Finished whole chain");
}
QString AuthContext::getStateMessage() const {
switch (m_accountState)
{
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();
}
}

View File

@ -0,0 +1,94 @@
#pragma once
#include <QObject>
#include <QList>
#include <QVector>
#include <QNetworkReply>
#include <QImage>
#include <katabasis/OAuth2.h>
#include "Yggdrasil.h"
#include "../AccountData.h"
#include "../AccountTask.h"
class AuthContext : public AccountTask
{
Q_OBJECT
public:
explicit AuthContext(AccountData * data, QObject *parent = 0);
bool isBusy() {
return m_activity != Katabasis::Activity::Idle;
};
Katabasis::Validity validity() {
return m_data->validity_;
};
//bool signOut();
QString getStateMessage() const override;
signals:
void activityChanged(Katabasis::Activity activity);
private slots:
// OAuth-specific callbacks
void onOAuthLinkingSucceeded();
void onOAuthLinkingFailed();
void onOpenBrowser(const QUrl &url);
void onCloseBrowser();
void onOAuthActivityChanged(Katabasis::Activity activity);
// 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>);
void doSTSAuthMinecraft();
Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doMinecraftAuth();
Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doSTSAuthGeneric();
Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doXBoxProfile();
Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doMinecraftProfile();
Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doGetSkin();
Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void checkResult();
protected:
void beginActivity(Katabasis::Activity activity);
void finishActivity();
void clearTokens();
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;
};

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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_;
};

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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();
}
}

View File

@ -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:
};

View File

@ -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);
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
changeState(STATE_WORKING);
// Get the content of the request we're going to send to the server.
QJsonDocument doc(getRequestContent());
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 YggdrasilTask::refreshTimers(qint64, qint64)
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 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)
{
changeState(STATE_PROCESSING_RESPONSE);
// 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_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;
}

View File

@ -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;
};