aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth/flows/AuthContext.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/auth/flows/AuthContext.cpp')
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp752
1 files changed, 752 insertions, 0 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();
+ }
+}