#include #include #include #include #include #include #include #include #include #include #include #include #include "context.h" #include "katabasis/Globals.h" #include "katabasis/StoreQSettings.h" #include "katabasis/Requestor.h" #include "BuildConfig.h" using OAuth2 = Katabasis::OAuth2; using Requestor = Katabasis::Requestor; using Activity = Katabasis::Activity; Context::Context(QObject *parent) : QObject(parent) { mgr = new QNetworkAccessManager(this); Katabasis::OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; opts.clientIdentifier = BuildConfig.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}; oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr); connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed); connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded); connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser); connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser); connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged); } void Context::beginActivity(Activity activity) { if(isBusy()) { throw 0; } activity_ = activity; emit activityChanged(activity_); } void Context::finishActivity() { if(!isBusy()) { throw 0; } activity_ = Katabasis::Activity::Idle; m_account.validity_ = m_account.minecraftProfile.validity; emit activityChanged(activity_); } QString Context::gameToken() { return m_account.minecraftToken.token; } QString Context::userId() { return m_account.minecraftProfile.id; } QString Context::userName() { return m_account.minecraftProfile.name; } bool Context::silentSignIn() { if(isBusy()) { return false; } beginActivity(Activity::Refreshing); if(!oauth2->refresh()) { finishActivity(); return false; } requestsDone = 0; xboxProfileSucceeded = false; mcAuthSucceeded = false; return true; } bool Context::signIn() { if(isBusy()) { return false; } requestsDone = 0; xboxProfileSucceeded = false; mcAuthSucceeded = false; beginActivity(Activity::LoggingIn); oauth2->unlink(); m_account = AccountData(); oauth2->link(); return true; } bool Context::signOut() { if(isBusy()) { return false; } beginActivity(Activity::LoggingOut); oauth2->unlink(); m_account = AccountData(); finishActivity(); return true; } void Context::onOpenBrowser(const QUrl &url) { QDesktopServices::openUrl(url); } void Context::onCloseBrowser() { } void Context::onLinkingFailed() { finishActivity(); } void Context::onLinkingSucceeded() { auto *o2t = qobject_cast(sender()); if (!o2t->linked()) { finishActivity(); 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 Context::onOAuthActivityChanged(Katabasis::Activity activity) { // respond to activity change here } void Context::doUserAuth() { 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_account.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, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::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::ISODateWithMs); 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 Context::onUserAuthDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; finishActivity(); return; } Katabasis::Token temp; if(!parseXTokenResponse(replyData, temp)) { qWarning() << "Could not parse user authentication response..."; finishActivity(); return; } m_account.userToken = temp; 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 Context::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_account.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, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Second layer of XBox auth ... commencing."; } void Context::onSTSAuthMinecraftDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; finishActivity(); return; } Katabasis::Token temp; if(!parseXTokenResponse(replyData, temp)) { qWarning() << "Could not parse authorization response for access to mojang services..."; finishActivity(); return; } if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) { qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; qDebug() << replyData; finishActivity(); return; } m_account.mojangservicesToken = temp; doMinecraftAuth(); } void Context::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_account.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, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Second layer of XBox auth ... commencing."; } void Context::onSTSAuthGenericDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; finishActivity(); return; } Katabasis::Token temp; if(!parseXTokenResponse(replyData, temp)) { qWarning() << "Could not parse authorization response for access to xbox API..."; finishActivity(); return; } if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) { qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; qDebug() << replyData; finishActivity(); return; } m_account.xboxApiToken = temp; doXBoxProfile(); } void Context::doMinecraftAuth() { QString mc_auth_template = R"XXX( { "identityToken": "XBL3.0 x=%1;%2" } )XXX"; auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.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, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::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 Context::onMinecraftAuthDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { requestsDone++; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; qDebug() << replyData; finishActivity(); return; } if(!parseMojangResponse(replyData, m_account.minecraftToken)) { qWarning() << "Could not parse login_with_xbox response..."; qDebug() << replyData; finishActivity(); return; } mcAuthSucceeded = true; checkResult(); } void Context::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_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8()); Requestor *requestor = new Requestor(mgr, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone); requestor->get(request); qDebug() << "Getting Xbox profile..."; } void Context::onXBoxProfileDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { requestsDone ++; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; qDebug() << replyData; finishActivity(); return; } qDebug() << "XBox profile: " << replyData; xboxProfileSucceeded = true; checkResult(); } void Context::checkResult() { if(requestsDone != 2) { return; } if(mcAuthSucceeded && xboxProfileSucceeded) { doMinecraftProfile(); } else { finishActivity(); } } 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 Context::doMinecraftProfile() { 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_account.minecraftToken.token).toUtf8()); Requestor *requestor = new Requestor(mgr, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone); requestor->get(request); } void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { qDebug() << data; if (error == QNetworkReply::ContentNotFoundError) { m_account.minecraftProfile = MinecraftProfile(); finishActivity(); return; } if (error != QNetworkReply::NoError) { finishActivity(); return; } if(!parseMinecraftProfile(data, m_account.minecraftProfile)) { m_account.minecraftProfile = MinecraftProfile(); finishActivity(); return; } doGetSkin(); } void Context::doGetSkin() { auto url = QUrl(m_account.minecraftProfile.skin.url); QNetworkRequest request = QNetworkRequest(url); Requestor *requestor = new Requestor(mgr, oauth2, this); requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &Context::onSkinDone); requestor->get(request); } void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList) { if (error == QNetworkReply::NoError) { m_account.minecraftProfile.skin.data = data; } finishActivity(); } namespace { void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { if(t.validity == Katabasis::Validity::None || !t.persistent) { return; } QJsonObject out; if(t.issueInstant.isValid()) { out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch()); } if(t.notAfter.isValid()) { out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch()); } if(!t.token.isEmpty()) { out["token"] = QJsonValue(t.token); } if(!t.refresh_token.isEmpty()) { out["refresh_token"] = QJsonValue(t.refresh_token); } if(t.extra.size()) { out["extra"] = QJsonObject::fromVariantMap(t.extra); } if(out.size()) { parent[tokenName] = out; } } Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) { Katabasis::Token out; auto tokenObject = parent.value(tokenName).toObject(); if(tokenObject.isEmpty()) { return out; } auto issueInstant = tokenObject.value("iat"); if(issueInstant.isDouble()) { out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble()); } auto notAfter = tokenObject.value("exp"); if(notAfter.isDouble()) { out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble()); } auto token = tokenObject.value("token"); if(token.isString()) { out.token = token.toString(); out.validity = Katabasis::Validity::Assumed; } auto refresh_token = tokenObject.value("refresh_token"); if(refresh_token.isString()) { out.refresh_token = refresh_token.toString(); } auto extra = tokenObject.value("extra"); if(extra.isObject()) { out.extra = extra.toObject().toVariantMap(); } return out; } void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) { if(p.id.isEmpty()) { return; } QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); if(p.currentCape != -1) { out["cape"] = p.capes[p.currentCape].id; } { QJsonObject skinObj; skinObj["id"] = p.skin.id; skinObj["url"] = p.skin.url; skinObj["variant"] = p.skin.variant; if(p.skin.data.size()) { skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); } out["skin"] = skinObj; } QJsonArray capesArray; for(auto & cape: p.capes) { QJsonObject capeObj; capeObj["id"] = cape.id; capeObj["url"] = cape.url; capeObj["alias"] = cape.alias; if(cape.data.size()) { capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); } capesArray.push_back(capeObj); } out["capes"] = capesArray; parent[tokenName] = out; } MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) { MinecraftProfile out; auto tokenObject = parent.value(tokenName).toObject(); if(tokenObject.isEmpty()) { return out; } { auto idV = tokenObject.value("id"); auto nameV = tokenObject.value("name"); if(!idV.isString() || !nameV.isString()) { qWarning() << "mandatory profile attributes are missing or of unexpected type"; return MinecraftProfile(); } out.name = nameV.toString(); out.id = idV.toString(); } { auto skinV = tokenObject.value("skin"); if(!skinV.isObject()) { qWarning() << "skin is missing"; return MinecraftProfile(); } auto skinObj = skinV.toObject(); auto idV = skinObj.value("id"); auto urlV = skinObj.value("url"); auto variantV = skinObj.value("variant"); if(!idV.isString() || !urlV.isString() || !variantV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); out.skin.variant = variantV.toString(); // data for skin is optional auto dataV = skinObj.value("data"); if(dataV.isString()) { // TODO: validate base64 out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "skin data is something unexpected"; return MinecraftProfile(); } } auto capesV = tokenObject.value("capes"); if(!capesV.isArray()) { qWarning() << "capes is not an array!"; return MinecraftProfile(); } auto capesArray = capesV.toArray(); for(auto capeV: capesArray) { if(!capeV.isObject()) { qWarning() << "cape is not an object!"; return MinecraftProfile(); } auto capeObj = capeV.toObject(); auto idV = capeObj.value("id"); auto urlV = capeObj.value("url"); auto aliasV = capeObj.value("alias"); if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); cape.alias = aliasV.toString(); // data for cape is optional. auto dataV = capeObj.value("data"); if(dataV.isString()) { // TODO: validate base64 cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "cape data is something unexpected"; return MinecraftProfile(); } out.capes.push_back(cape); } out.validity = Katabasis::Validity::Assumed; return out; } } bool Context::resumeFromState(QByteArray data) { QJsonParseError error; auto doc = QJsonDocument::fromJson(data, &error); if(error.error != QJsonParseError::NoError) { qWarning() << "Failed to parse account data as JSON."; return false; } auto docObject = doc.object(); m_account.msaToken = tokenFromJSON(docObject, "msa"); m_account.userToken = tokenFromJSON(docObject, "utoken"); m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main"); m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc"); m_account.minecraftToken = tokenFromJSON(docObject, "ygg"); m_account.minecraftProfile = profileFromJSON(docObject, "profile"); m_account.validity_ = m_account.minecraftProfile.validity; return true; } QByteArray Context::saveState() { QJsonDocument doc; QJsonObject output; tokenToJSON(output, m_account.msaToken, "msa"); tokenToJSON(output, m_account.userToken, "utoken"); tokenToJSON(output, m_account.xboxApiToken, "xrp-main"); tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc"); tokenToJSON(output, m_account.minecraftToken, "ygg"); profileToJSON(output, m_account.minecraftProfile, "profile"); doc.setObject(output); return doc.toJson(QJsonDocument::Indented); }