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,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

@ -0,0 +1,337 @@
/* 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 "Yggdrasil.h"
#include "../AccountData.h"
#include <QObject>
#include <QString>
#include <QJsonObject>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QByteArray>
#include <Env.h>
#include <BuildConfig.h>
#include <QDebug>
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
: AccountTask(data, parent)
{
changeState(STATE_CREATED);
}
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
changeState(STATE_WORKING);
QNetworkRequest netRequest(endpoint);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
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, &Yggdrasil::abortByTimeout);
connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
}
void Yggdrasil::executeTask() {
}
void Yggdrasil::refresh() {
start();
/*
* {
* "clientToken": "client identifier"
* "accessToken": "current access token to be refreshed"
* "selectedProfile": // specifying this causes errors
* {
* "id": "profile ID"
* "name": "profile name"
* }
* "requestUser": true/false // request the user structure
* }
*/
QJsonObject req;
req.insert("clientToken", m_data->clientToken());
req.insert("accessToken", m_data->accessToken());
/*
{
auto currentProfile = m_account->currentProfile();
QJsonObject profile;
profile.insert("id", currentProfile->id());
profile.insert("name", currentProfile->name());
req.insert("selectedProfile", profile);
}
*/
req.insert("requestUser", false);
QJsonDocument doc(req);
QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh");
QByteArray requestData = doc.toJson();
sendRequest(reqUrl, requestData);
}
void Yggdrasil::login(QString password) {
start();
/*
* {
* "agent": { // optional
* "name": "Minecraft", // So far this is the only encountered value
* "version": 1 // This number might be increased
* // by the vanilla client in the future
* },
* "username": "mojang account name", // Can be an email address or player name for
* // unmigrated accounts
* "password": "mojang account password",
* "clientToken": "client identifier", // optional
* "requestUser": true/false // request the user structure
* }
*/
QJsonObject req;
{
QJsonObject agent;
// C++ makes string literals void* for some stupid reason, so we have to tell it
// QString... Thanks Obama.
agent.insert("name", QString("Minecraft"));
agent.insert("version", 1);
req.insert("agent", agent);
}
req.insert("username", m_data->userName());
req.insert("password", password);
req.insert("requestUser", false);
// If we already have a client token, give it to the server.
// Otherwise, let the server give us one.
m_data->generateClientTokenIfMissing();
req.insert("clientToken", m_data->clientToken());
QJsonDocument doc(req);
QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate");
QNetworkRequest netRequest(reqUrl);
QByteArray requestData = doc.toJson();
sendRequest(reqUrl, requestData);
}
void Yggdrasil::refreshTimers(qint64, qint64)
{
timeout_keeper.stop();
timeout_keeper.start(timeout_max);
progress(count = 0, timeout_max);
}
void Yggdrasil::heartbeat()
{
count += time_step;
progress(count, timeout_max);
}
bool Yggdrasil::abort()
{
progress(timeout_max, timeout_max);
// TODO: actually use this in a meaningful way
m_aborted = Yggdrasil::BY_USER;
m_netReply->abort();
return true;
}
void Yggdrasil::abortByTimeout()
{
progress(timeout_max, timeout_max);
// TODO: actually use this in a meaningful way
m_aborted = Yggdrasil::BY_TIMEOUT;
m_netReply->abort();
}
void Yggdrasil::sslErrors(QList<QSslError> errors)
{
int i = 1;
for (auto error : errors)
{
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void Yggdrasil::processResponse(QJsonObject responseData)
{
// Read the response data. We need to get the client token, access token, and the selected
// profile.
qDebug() << "Processing authentication response.";
// qDebug() << responseData;
// If we already have a client token, make sure the one the server gave us matches our
// existing one.
QString clientToken = responseData.value("clientToken").toString("");
if (clientToken.isEmpty())
{
// Fail if the server gave us an empty client token
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
return;
}
if(m_data->clientToken().isEmpty()) {
m_data->setClientToken(clientToken);
}
else if(clientToken != m_data->clientToken()) {
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
return;
}
// Now, we set the access token.
qDebug() << "Getting access token.";
QString accessToken = responseData.value("accessToken").toString("");
if (accessToken.isEmpty())
{
// Fail if the server didn't give us an access token.
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
return;
}
// Set the access token.
m_data->yggdrasilToken.token = accessToken;
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
qDebug() << "Finished reading authentication response.";
changeState(STATE_SUCCEEDED);
}
void Yggdrasil::processReply()
{
changeState(STATE_WORKING);
switch (m_netReply->error())
{
case QNetworkReply::NoError:
break;
case QNetworkReply::TimeoutError:
changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
return;
case QNetworkReply::OperationCanceledError:
changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
return;
case QNetworkReply::SslHandshakeFailedError:
changeState(
STATE_FAILED_SOFT,
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
"<li>You use Windows XP and need to <a "
"href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
"your root certificates</a></li>"
"<li>Some device on your network is interfering with SSL traffic. In that case, "
"you have bigger worries than Minecraft not starting.</li>"
"<li>Possibly something else. Check the MultiMC log file for details</li>"
"</ul>"));
return;
// used for invalid credentials and similar errors. Fall through.
case QNetworkReply::ContentAccessDenied:
case QNetworkReply::ContentOperationNotPermittedError:
break;
default:
changeState(STATE_FAILED_SOFT,
tr("Authentication operation failed due to a network error: %1 (%2)")
.arg(m_netReply->errorString()).arg(m_netReply->error()));
return;
}
// Try to parse the response regardless of the response code.
// Sometimes the auth server will give more information and an error code.
QJsonParseError jsonError;
QByteArray replyData = m_netReply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
// Check the response code.
int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (responseCode == 200)
{
// If the response code was 200, then there shouldn't be an error. Make sure
// anyways.
// Also, sometimes an empty reply indicates success. If there was no data received,
// pass an empty json object to the processResponse function.
if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
{
processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
return;
}
else
{
changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response "
"JSON response: %1 at offset %2.")
.arg(jsonError.errorString())
.arg(jsonError.offset));
qCritical() << replyData;
}
return;
}
// If the response code was not 200, then Yggdrasil may have given us information
// about the error.
// If we can parse the response, then get information from it. Otherwise just say
// there was an unknown error.
if (jsonError.error == QJsonParseError::NoError)
{
// We were able to parse the server's response. Woo!
// Call processError. If a subclass has overridden it then they'll handle their
// stuff there.
qDebug() << "The request failed, but the server gave us an error message. "
"Processing error.";
processError(doc.object());
}
else
{
// The server didn't say anything regarding the error. Give the user an unknown
// error.
qDebug()
<< "The request failed and the server gave no error message. Unknown error.";
changeState(STATE_FAILED_SOFT,
tr("An unknown error occurred when trying to communicate with the "
"authentication server: %1").arg(m_netReply->errorString()));
}
}
void Yggdrasil::processError(QJsonObject responseData)
{
QJsonValue errorVal = responseData.value("error");
QJsonValue errorMessageValue = responseData.value("errorMessage");
QJsonValue causeVal = responseData.value("cause");
if (errorVal.isString() && errorMessageValue.isString())
{
m_error = std::shared_ptr<Error>(new Error{
errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")});
changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
}
else
{
// Error is not in standard format. Don't set m_error and return unknown error.
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
}
}

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