aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth/flows
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/auth/flows')
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp752
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.h94
-rw-r--r--launcher/minecraft/auth/flows/AuthenticateTask.cpp202
-rw-r--r--launcher/minecraft/auth/flows/AuthenticateTask.h46
-rw-r--r--launcher/minecraft/auth/flows/MSAHelper.txt51
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.cpp20
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.h10
-rw-r--r--launcher/minecraft/auth/flows/MSASilent.cpp16
-rw-r--r--launcher/minecraft/auth/flows/MSASilent.h10
-rw-r--r--launcher/minecraft/auth/flows/MojangLogin.cpp14
-rw-r--r--launcher/minecraft/auth/flows/MojangLogin.h13
-rw-r--r--launcher/minecraft/auth/flows/MojangRefresh.cpp14
-rw-r--r--launcher/minecraft/auth/flows/MojangRefresh.h10
-rw-r--r--launcher/minecraft/auth/flows/RefreshTask.cpp144
-rw-r--r--launcher/minecraft/auth/flows/RefreshTask.h44
-rw-r--r--launcher/minecraft/auth/flows/ValidateTask.cpp61
-rw-r--r--launcher/minecraft/auth/flows/ValidateTask.h47
-rw-r--r--launcher/minecraft/auth/flows/Yggdrasil.cpp337
-rw-r--r--launcher/minecraft/auth/flows/Yggdrasil.h82
19 files changed, 1423 insertions, 544 deletions
diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp
new file mode 100644
index 00000000..9aa58ac3
--- /dev/null
+++ b/launcher/minecraft/auth/flows/AuthContext.cpp
@@ -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();
+ }
+}
diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h
new file mode 100644
index 00000000..5f99dba3
--- /dev/null
+++ b/launcher/minecraft/auth/flows/AuthContext.h
@@ -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;
+};
diff --git a/launcher/minecraft/auth/flows/AuthenticateTask.cpp b/launcher/minecraft/auth/flows/AuthenticateTask.cpp
deleted file mode 100644
index 2e8dc859..00000000
--- a/launcher/minecraft/auth/flows/AuthenticateTask.cpp
+++ /dev/null
@@ -1,202 +0,0 @@
-
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "AuthenticateTask.h"
-#include "../MojangAccount.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QVariant>
-
-#include <QDebug>
-#include <QUuid>
-
-AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
- QObject *parent)
- : YggdrasilTask(account, parent), m_password(password)
-{
-}
-
-QJsonObject AuthenticateTask::getRequestContent() const
-{
- /*
- * {
- * "agent": { // optional
- * "name": "Minecraft", // So far this is the only encountered value
- * "version": 1 // This number might be increased
- * // by the vanilla client in the future
- * },
- * "username": "mojang account name", // Can be an email address or player name for
- // unmigrated accounts
- * "password": "mojang account password",
- * "clientToken": "client identifier" // optional
- * "requestUser": true/false // request the user structure
- * }
- */
- QJsonObject req;
-
- {
- QJsonObject agent;
- // C++ makes string literals void* for some stupid reason, so we have to tell it
- // QString... Thanks Obama.
- agent.insert("name", QString("Minecraft"));
- agent.insert("version", 1);
- req.insert("agent", agent);
- }
-
- req.insert("username", m_account->username());
- req.insert("password", m_password);
- req.insert("requestUser", true);
-
- // If we already have a client token, give it to the server.
- // Otherwise, let the server give us one.
-
- if(m_account->m_clientToken.isEmpty())
- {
- auto uuid = QUuid::createUuid();
- auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
- m_account->m_clientToken = uuidString;
- }
- req.insert("clientToken", m_account->m_clientToken);
-
- return req;
-}
-
-void AuthenticateTask::processResponse(QJsonObject responseData)
-{
- // Read the response data. We need to get the client token, access token, and the selected
- // profile.
- qDebug() << "Processing authentication response.";
- // qDebug() << responseData;
- // If we already have a client token, make sure the one the server gave us matches our
- // existing one.
- qDebug() << "Getting client token.";
- QString clientToken = responseData.value("clientToken").toString("");
- if (clientToken.isEmpty())
- {
- // Fail if the server gave us an empty client token
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
- return;
- }
- if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
- return;
- }
- // Set the client token.
- m_account->m_clientToken = clientToken;
-
- // Now, we set the access token.
- qDebug() << "Getting access token.";
- QString accessToken = responseData.value("accessToken").toString("");
- if (accessToken.isEmpty())
- {
- // Fail if the server didn't give us an access token.
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
- return;
- }
- // Set the access token.
- m_account->m_accessToken = accessToken;
-
- // Now we load the list of available profiles.
- // Mojang hasn't yet implemented the profile system,
- // but we might as well support what's there so we
- // don't have trouble implementing it later.
- qDebug() << "Loading profile list.";
- QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
- QList<AccountProfile> loadedProfiles;
- for (auto iter : availableProfiles)
- {
- QJsonObject profile = iter.toObject();
- // Profiles are easy, we just need their ID and name.
- QString id = profile.value("id").toString("");
- QString name = profile.value("name").toString("");
- bool legacy = profile.value("legacy").toBool(false);
-
- if (id.isEmpty() || name.isEmpty())
- {
- // This should never happen, but we might as well
- // warn about it if it does so we can debug it easily.
- // You never know when Mojang might do something truly derpy.
- qWarning() << "Found entry in available profiles list with missing ID or name "
- "field. Ignoring it.";
- }
-
- // Now, add a new AccountProfile entry to the list.
- loadedProfiles.append({id, name, legacy});
- }
- // Put the list of profiles we loaded into the MojangAccount object.
- m_account->m_profiles = loadedProfiles;
-
- // Finally, we set the current profile to the correct value. This is pretty simple.
- // We do need to make sure that the current profile that the server gave us
- // is actually in the available profiles list.
- // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
- qDebug() << "Setting current profile.";
- QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
- QString currentProfileId = currentProfile.value("id").toString("");
- if (currentProfileId.isEmpty())
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
- return;
- }
- if (!m_account->setCurrentProfile(currentProfileId))
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
- return;
- }
-
- // this is what the vanilla launcher passes to the userProperties launch param
- if (responseData.contains("user"))
- {
- User u;
- auto obj = responseData.value("user").toObject();
- u.id = obj.value("id").toString();
- auto propArray = obj.value("properties").toArray();
- for (auto prop : propArray)
- {
- auto propTuple = prop.toObject();
- auto name = propTuple.value("name").toString();
- auto value = propTuple.value("value").toString();
- u.properties.insert(name, value);
- }
- m_account->m_user = u;
- }
-
- // We've made it through the minefield of possible errors. Return true to indicate that
- // we've succeeded.
- qDebug() << "Finished reading authentication response.";
- changeState(STATE_SUCCEEDED);
-}
-
-QString AuthenticateTask::getEndpoint() const
-{
- return "authenticate";
-}
-
-QString AuthenticateTask::getStateMessage() const
-{
- switch (m_state)
- {
- case STATE_SENDING_REQUEST:
- return tr("Authenticating: Sending request...");
- case STATE_PROCESSING_RESPONSE:
- return tr("Authenticating: Processing response...");
- default:
- return YggdrasilTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/AuthenticateTask.h b/launcher/minecraft/auth/flows/AuthenticateTask.h
deleted file mode 100644
index 4c14eec7..00000000
--- a/launcher/minecraft/auth/flows/AuthenticateTask.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "../YggdrasilTask.h"
-
-#include <QObject>
-#include <QString>
-#include <QJsonObject>
-
-/**
- * The authenticate task takes a MojangAccount with no access token and password and attempts to
- * authenticate with Mojang's servers.
- * If successful, it will set the MojangAccount's access token.
- */
-class AuthenticateTask : public YggdrasilTask
-{
- Q_OBJECT
-public:
- AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
-
-protected:
- virtual QJsonObject getRequestContent() const override;
-
- virtual QString getEndpoint() const override;
-
- virtual void processResponse(QJsonObject responseData) override;
-
- virtual QString getStateMessage() const override;
-
-private:
- QString m_password;
-};
diff --git a/launcher/minecraft/auth/flows/MSAHelper.txt b/launcher/minecraft/auth/flows/MSAHelper.txt
new file mode 100644
index 00000000..dfaec374
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSAHelper.txt
@@ -0,0 +1,51 @@
+class Helper : public QObject {
+ Q_OBJECT
+
+public:
+ Helper(MSAFlows * context) : QObject(), context_(context), msg_(QString()) {
+ QFile tokenCache("usercache.dat");
+ if(tokenCache.open(QIODevice::ReadOnly)) {
+ context_->resumeFromState(tokenCache.readAll());
+ }
+ }
+
+public slots:
+ void run() {
+ connect(context_, &MSAFlows::activityChanged, this, &Helper::onActivityChanged);
+ context_->silentSignIn();
+ }
+
+ void onFailed() {
+ qDebug() << "Login failed";
+ }
+
+ void onActivityChanged(Katabasis::Activity activity) {
+ if(activity == Katabasis::Activity::Idle) {
+ switch(context_->validity()) {
+ case Katabasis::Validity::None: {
+ // account is gone, remove it.
+ QFile::remove("usercache.dat");
+ }
+ break;
+ case Katabasis::Validity::Assumed: {
+ // this is basically a soft-failed refresh. do nothing.
+ }
+ break;
+ case Katabasis::Validity::Certain: {
+ // stuff got refreshed / signed in. Save.
+ auto data = context_->saveState();
+ QSaveFile tokenCache("usercache.dat");
+ if(tokenCache.open(QIODevice::WriteOnly)) {
+ tokenCache.write(context_->saveState());
+ tokenCache.commit();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+private:
+ MSAFlows *context_;
+ QString msg_;
+};
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp
new file mode 100644
index 00000000..03beb279
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp
@@ -0,0 +1,20 @@
+#include "MSAInteractive.h"
+
+MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
+
+void MSAInteractive::executeTask() {
+ m_requestsDone = 0;
+ m_xboxProfileSucceeded = false;
+ m_mcAuthSucceeded = false;
+
+ initMSA();
+
+ QVariantMap extraOpts;
+ extraOpts["prompt"] = "select_account";
+ m_oauth2->setExtraRequestParams(extraOpts);
+
+ beginActivity(Katabasis::Activity::LoggingIn);
+ m_oauth2->unlink();
+ *m_data = AccountData();
+ m_oauth2->link();
+}
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h
new file mode 100644
index 00000000..9556f254
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSAInteractive.h
@@ -0,0 +1,10 @@
+#pragma once
+#include "AuthContext.h"
+
+class MSAInteractive : public AuthContext
+{
+ Q_OBJECT
+public:
+ explicit MSAInteractive(AccountData * data, QObject *parent = 0);
+ void executeTask() override;
+};
diff --git a/launcher/minecraft/auth/flows/MSASilent.cpp b/launcher/minecraft/auth/flows/MSASilent.cpp
new file mode 100644
index 00000000..8ce43c1f
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSASilent.cpp
@@ -0,0 +1,16 @@
+#include "MSASilent.h"
+
+MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
+
+void MSASilent::executeTask() {
+ m_requestsDone = 0;
+ m_xboxProfileSucceeded = false;
+ m_mcAuthSucceeded = false;
+
+ initMSA();
+
+ beginActivity(Katabasis::Activity::Refreshing);
+ if(!m_oauth2->refresh()) {
+ finishActivity();
+ }
+}
diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h
new file mode 100644
index 00000000..e1b3d43d
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MSASilent.h
@@ -0,0 +1,10 @@
+#pragma once
+#include "AuthContext.h"
+
+class MSASilent : public AuthContext
+{
+ Q_OBJECT
+public:
+ explicit MSASilent(AccountData * data, QObject *parent = 0);
+ void executeTask() override;
+};
diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp
new file mode 100644
index 00000000..cca911b5
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangLogin.cpp
@@ -0,0 +1,14 @@
+#include "MojangLogin.h"
+
+MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthContext(data, parent), m_password(password) {}
+
+void MojangLogin::executeTask() {
+ m_requestsDone = 0;
+ m_xboxProfileSucceeded = false;
+ m_mcAuthSucceeded = false;
+
+ initMojang();
+
+ beginActivity(Katabasis::Activity::LoggingIn);
+ m_yggdrasil->login(m_password);
+}
diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h
new file mode 100644
index 00000000..2e765ae8
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangLogin.h
@@ -0,0 +1,13 @@
+#pragma once
+#include "AuthContext.h"
+
+class MojangLogin : public AuthContext
+{
+ Q_OBJECT
+public:
+ explicit MojangLogin(AccountData * data, QString password, QObject *parent = 0);
+ void executeTask() override;
+
+private:
+ QString m_password;
+};
diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp
new file mode 100644
index 00000000..af99175c
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangRefresh.cpp
@@ -0,0 +1,14 @@
+#include "MojangRefresh.h"
+
+MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
+
+void MojangRefresh::executeTask() {
+ m_requestsDone = 0;
+ m_xboxProfileSucceeded = false;
+ m_mcAuthSucceeded = false;
+
+ initMojang();
+
+ beginActivity(Katabasis::Activity::Refreshing);
+ m_yggdrasil->refresh();
+}
diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h
new file mode 100644
index 00000000..fb4facd5
--- /dev/null
+++ b/launcher/minecraft/auth/flows/MojangRefresh.h
@@ -0,0 +1,10 @@
+#pragma once
+#include "AuthContext.h"
+
+class MojangRefresh : public AuthContext
+{
+ Q_OBJECT
+public:
+ explicit MojangRefresh(AccountData * data, QObject *parent = 0);
+ void executeTask() override;
+};
diff --git a/launcher/minecraft/auth/flows/RefreshTask.cpp b/launcher/minecraft/auth/flows/RefreshTask.cpp
deleted file mode 100644
index ecba178d..00000000
--- a/launcher/minecraft/auth/flows/RefreshTask.cpp
+++ /dev/null
@@ -1,144 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "RefreshTask.h"
-#include "../MojangAccount.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QVariant>
-
-#include <QDebug>
-
-RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
-{
-}
-
-QJsonObject RefreshTask::getRequestContent() const
-{
- /*
- * {
- * "clientToken": "client identifier"
- * "accessToken": "current access token to be refreshed"
- * "selectedProfile": // specifying this causes errors
- * {
- * "id": "profile ID"
- * "name": "profile name"
- * }
- * "requestUser": true/false // request the user structure
- * }
- */
- QJsonObject req;
- req.insert("clientToken", m_account->m_clientToken);
- req.insert("accessToken", m_account->m_accessToken);
- /*
- {
- auto currentProfile = m_account->currentProfile();
- QJsonObject profile;
- profile.insert("id", currentProfile->id());
- profile.insert("name", currentProfile->name());
- req.insert("selectedProfile", profile);
- }
- */
- req.insert("requestUser", true);
-
- return req;
-}
-
-void RefreshTask::processResponse(QJsonObject responseData)
-{
- // Read the response data. We need to get the client token, access token, and the selected
- // profile.
- qDebug() << "Processing authentication response.";
-
- // qDebug() << responseData;
- // If we already have a client token, make sure the one the server gave us matches our
- // existing one.
- QString clientToken = responseData.value("clientToken").toString("");
- if (clientToken.isEmpty())
- {
- // Fail if the server gave us an empty client token
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
- return;
- }
- if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
- return;
- }
-
- // Now, we set the access token.
- qDebug() << "Getting new access token.";
- QString accessToken = responseData.value("accessToken").toString("");
- if (accessToken.isEmpty())
- {
- // Fail if the server didn't give us an access token.
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
- return;
- }
-
- // we validate that the server responded right. (our current profile = returned current
- // profile)
- QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
- QString currentProfileId = currentProfile.value("id").toString("");
- if (m_account->currentProfile()->id != currentProfileId)
- {
- changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
- return;
- }
-
- // this is what the vanilla launcher passes to the userProperties launch param
- if (responseData.contains("user"))
- {
- User u;
- auto obj = responseData.value("user").toObject();
- u.id = obj.value("id").toString();
- auto propArray = obj.value("properties").toArray();
- for (auto prop : propArray)
- {
- auto propTuple = prop.toObject();
- auto name = propTuple.value("name").toString();
- auto value = propTuple.value("value").toString();
- u.properties.insert(name, value);
- }
- m_account->m_user = u;
- }
-
- // We've made it through the minefield of possible errors. Return true to indicate that
- // we've succeeded.
- qDebug() << "Finished reading refresh response.";
- // Reset the access token.
- m_account->m_accessToken = accessToken;
- changeState(STATE_SUCCEEDED);
-}
-
-QString RefreshTask::getEndpoint() const
-{
- return "refresh";
-}
-
-QString RefreshTask::getStateMessage() const
-{
- switch (m_state)
- {
- case STATE_SENDING_REQUEST:
- return tr("Refreshing login token...");
- case STATE_PROCESSING_RESPONSE:
- return tr("Refreshing login token: Processing response...");
- default:
- return YggdrasilTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/RefreshTask.h b/launcher/minecraft/auth/flows/RefreshTask.h
deleted file mode 100644
index f0840dda..00000000
--- a/launcher/minecraft/auth/flows/RefreshTask.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "../YggdrasilTask.h"
-
-#include <QObject>
-#include <QString>
-#include <QJsonObject>
-
-/**
- * The authenticate task takes a MojangAccount with a possibly timed-out access token
- * and attempts to authenticate with Mojang's servers.
- * If successful, it will set the new access token. The token is considered validated.
- */
-class RefreshTask : public YggdrasilTask
-{
- Q_OBJECT
-public:
- RefreshTask(MojangAccount * account);
-
-protected:
- virtual QJsonObject getRequestContent() const override;
-
- virtual QString getEndpoint() const override;
-
- virtual void processResponse(QJsonObject responseData) override;
-
- virtual QString getStateMessage() const override;
-};
-
diff --git a/launcher/minecraft/auth/flows/ValidateTask.cpp b/launcher/minecraft/auth/flows/ValidateTask.cpp
deleted file mode 100644
index 6b3f0a65..00000000
--- a/launcher/minecraft/auth/flows/ValidateTask.cpp
+++ /dev/null
@@ -1,61 +0,0 @@
-
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "ValidateTask.h"
-#include "../MojangAccount.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QVariant>
-
-#include <QDebug>
-
-ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
- : YggdrasilTask(account, parent)
-{
-}
-
-QJsonObject ValidateTask::getRequestContent() const
-{
- QJsonObject req;
- req.insert("accessToken", m_account->m_accessToken);
- return req;
-}
-
-void ValidateTask::processResponse(QJsonObject responseData)
-{
- // Assume that if processError wasn't called, then the request was successful.
- changeState(YggdrasilTask::STATE_SUCCEEDED);
-}
-
-QString ValidateTask::getEndpoint() const
-{
- return "validate";
-}
-
-QString ValidateTask::getStateMessage() const
-{
- switch (m_state)
- {
- case YggdrasilTask::STATE_SENDING_REQUEST:
- return tr("Validating access token: Sending request...");
- case YggdrasilTask::STATE_PROCESSING_RESPONSE:
- return tr("Validating access token: Processing response...");
- default:
- return YggdrasilTask::getStateMessage();
- }
-}
diff --git a/launcher/minecraft/auth/flows/ValidateTask.h b/launcher/minecraft/auth/flows/ValidateTask.h
deleted file mode 100644
index 986c2e9f..00000000
--- a/launcher/minecraft/auth/flows/ValidateTask.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
- */
-
-#pragma once
-
-#include "../YggdrasilTask.h"
-
-#include <QObject>
-#include <QString>
-#include <QJsonObject>
-
-/**
- * The validate task takes a MojangAccount and checks to make sure its access token is valid.
- */
-class ValidateTask : public YggdrasilTask
-{
- Q_OBJECT
-public:
- ValidateTask(MojangAccount *account, QObject *parent = 0);
-
-protected:
- virtual QJsonObject getRequestContent() const override;
-
- virtual QString getEndpoint() const override;
-
- virtual void processResponse(QJsonObject responseData) override;
-
- virtual QString getStateMessage() const override;
-
-private:
-};
diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp
new file mode 100644
index 00000000..7cea059c
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp
@@ -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."));
+ }
+}
diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/flows/Yggdrasil.h
new file mode 100644
index 00000000..e709cb9f
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Yggdrasil.h
@@ -0,0 +1,82 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "../AccountTask.h"
+
+#include <QString>
+#include <QJsonObject>
+#include <QTimer>
+#include <qsslerror.h>
+
+#include "../MinecraftAccount.h"
+
+class QNetworkReply;
+
+/**
+ * A Yggdrasil task is a task that performs an operation on a given mojang account.
+ */
+class Yggdrasil : public AccountTask
+{
+ Q_OBJECT
+public:
+ explicit Yggdrasil(AccountData * data, QObject *parent = 0);
+ virtual ~Yggdrasil() {};
+
+ void refresh();
+ void login(QString password);
+protected:
+ void executeTask() override;
+
+ /**
+ * Processes the response received from the server.
+ * If an error occurred, this should emit a failed signal.
+ * If Yggdrasil gave an error response, it should call setError() first, and then return false.
+ * Otherwise, it should return true.
+ * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
+ * an empty QJsonObject.
+ */
+ void processResponse(QJsonObject responseData);
+
+ /**
+ * Processes an error response received from the server.
+ * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
+ * \returns a QString error message that will be passed to emitFailed.
+ */
+ virtual void processError(QJsonObject responseData);
+
+protected slots:
+ void processReply();
+ void refreshTimers(qint64, qint64);
+ void heartbeat();
+ void sslErrors(QList<QSslError>);
+ void abortByTimeout();
+
+public slots:
+ virtual bool abort() override;
+
+private:
+ void sendRequest(QUrl endpoint, QByteArray content);
+
+protected:
+ QNetworkReply *m_netReply = nullptr;
+ QTimer timeout_keeper;
+ QTimer counter;
+ int count = 0; // num msec since time reset
+
+ const int timeout_max = 30000;
+ const int time_step = 50;
+};