diff options
Diffstat (limited to 'launcher/minecraft/auth/flows')
-rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.cpp | 343 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.h | 6 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/Parsers.cpp | 315 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/Parsers.h | 19 |
4 files changed, 392 insertions, 291 deletions
diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9fb3ec48..17fcc9dc 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -22,6 +22,8 @@ #include "Env.h" +#include "Parsers.h" + using OAuth2 = Katabasis::OAuth2; using Activity = Katabasis::Activity; @@ -86,7 +88,7 @@ void AuthContext::initMojang() { } void AuthContext::onMojangSucceeded() { - doMinecraftProfile(); + doEntitlements(); } @@ -169,137 +171,6 @@ void AuthContext::doUserAuth() { 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; -} - -bool getNumber(QJsonValue value, int64_t & out) { - if(!value.isDouble()) { - return false; - } - out = (int64_t) value.toDouble(); - return true; -} - -bool getBool(QJsonValue value, bool & out) { - if(!value.isBool()) { - return false; - } - out = value.toBool(); - 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, const char * name) { - qDebug() << "Parsing" << name <<":"; -#ifndef NDEBUG - qDebug() << data; -#endif - 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(); - return false; - } - - auto obj = doc.object(); - if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { - qWarning() << "User IssueInstant is not a timestamp"; - return false; - } - if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { - qWarning() << "User NotAfter is not a timestamp"; - return false; - } - if(!getString(obj.value("Token"), output.token)) { - qWarning() << "User Token is not a timestamp"; - return false; - } - auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); - if(!arrayVal.isArray()) { - qWarning() << "Missing xui claims array"; - 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..."; - return false; - } - output.extra[iter.key()] = claim; - } - - break; - } - if(!foundUHS) { - qWarning() << "Missing uhs"; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << name << "is valid."; - return true; -} - -} - void AuthContext::onUserAuthDone( QNetworkReply::NetworkError error, QByteArray replyData, @@ -313,7 +184,7 @@ void AuthContext::onUserAuthDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "UToken")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; finishActivity(); changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); @@ -374,7 +245,7 @@ void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray int64_t errorCode = -1; auto obj = doc.object(); - if(!getNumber(obj.value("XErr"), errorCode)) { + if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { qWarning() << "XErr is not a number"; return; } @@ -400,7 +271,7 @@ void AuthContext::onSTSAuthMinecraftDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { qWarning() << "Could not parse authorization response for access to mojang services..."; failResult(m_mcAuthSucceeded); return; @@ -417,67 +288,33 @@ void AuthContext::onSTSAuthMinecraftDone( } void AuthContext::doMinecraftAuth() { + auto requestURL = "https://api.minecraftservices.com/launcher/login"; + auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); + auto xToken = m_data->mojangservicesToken.token; + QString mc_auth_template = R"XXX( { - "identityToken": "XBL3.0 x=%1;%2" + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" } )XXX"; - auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->mojangservicesToken.token); + auto requestBody = mc_auth_template.arg(uhs, xToken); - QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox")); + QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); AuthRequest *requestor = new AuthRequest(this); connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftAuthDone); - requestor->post(request, data.toUtf8()); + requestor->post(request, requestBody.toUtf8()); qDebug() << "Getting Minecraft access token..."; } -namespace { -bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { - QJsonParseError jsonError; - qDebug() << "Parsing Mojang response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox as JSON: " << jsonError.errorString(); - 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"; - 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"; - return false; - } - - // TODO: it's a JWT... validate it? - if(!getString(obj.value("access_token"), output.token)) { - qWarning() << "access_token is not valid"; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << "Mojang response is valid."; - return true; -} -} - void AuthContext::onMinecraftAuthDone( QNetworkReply::NetworkError error, QByteArray replyData, QList<QNetworkReply::RawHeaderPair> headers ) { + qDebug() << replyData; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG @@ -487,7 +324,7 @@ void AuthContext::onMinecraftAuthDone( return; } - if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { + if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; #ifndef NDEBUG qDebug() << replyData; @@ -539,7 +376,7 @@ void AuthContext::onSTSAuthGenericDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { qWarning() << "Could not parse authorization response for access to xbox API..."; failResult(m_xboxProfileSucceeded); return; @@ -619,7 +456,7 @@ void AuthContext::checkResult() { return; } if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { - doMinecraftProfile(); + doEntitlements(); } else { finishActivity(); @@ -662,84 +499,33 @@ void AuthContext::checkResult() { } } -namespace { -bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { - qDebug() << "Parsing Minecraft profile..."; +void AuthContext::doEntitlements() { + auto uuid = QUuid::createUuid(); + entitlementsRequestId = uuid.toString().remove('{').remove('}'); + auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + entitlementsRequestId; + 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()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &AuthContext::onEntitlementsDone); + requestor->get(request); + qDebug() << "Getting Xbox profile..."; +} + + +void AuthContext::onEntitlementsDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { #ifndef NDEBUG qDebug() << data; #endif - - 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(); - return false; - } - - auto obj = doc.object(); - if(!getString(obj.value("id"), output.id)) { - qWarning() << "Minecraft profile id is not a string"; - return false; - } - - if(!getString(obj.value("name"), output.name)) { - qWarning() << "Minecraft profile name is not a string"; - 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(); - - QString currentCape; - for(auto cape: capesArray) { - 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 = capeOut.id; - } - if(!getString(capeObj.value("url"), capeOut.url)) { - continue; - } - if(!getString(capeObj.value("alias"), capeOut.alias)) { - continue; - } - - output.capes[capeOut.id] = capeOut; - } - output.currentCape = currentCape; - output.validity = Katabasis::Validity::Certain; - return true; -} + // TODO: check presence of same entitlementsRequestId? + // TODO: validate JWTs? + Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + doMinecraftProfile(); } void AuthContext::doMinecraftProfile() { @@ -766,9 +552,9 @@ void AuthContext::onMinecraftProfileDone( qDebug() << data; #endif if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. m_data->minecraftProfile = MinecraftProfile(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); + succeed(); return; } if (error != QNetworkReply::NoError) { @@ -776,7 +562,7 @@ void AuthContext::onMinecraftProfileDone( changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); return; } - if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { + if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); @@ -805,43 +591,13 @@ void AuthContext::doMigrationEligibilityCheck() { requestor->get(request); } -bool parseRolloutResponse(QByteArray & data, bool& result) { - qDebug() << "Parsing Rollout response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - QString feature; - if(!getString(obj.value("feature"), feature)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - if(feature != "msamigration") { - qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; - return false; - } - if(!getBool(obj.value("rollout"), result)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - return true; -} - void AuthContext::onMigrationEligibilityCheckDone( QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers ) { if (error == QNetworkReply::NoError) { - parseRolloutResponse(data, m_data->canMigrateToMSA); + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); } doGetSkin(); } @@ -865,6 +621,11 @@ void AuthContext::onSkinDone( if (error == QNetworkReply::NoError) { m_data->minecraftProfile.skin.data = data; } + succeed(); + +} + +void AuthContext::succeed() { m_data->validity_ = Katabasis::Validity::Certain; finishActivity(); changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index dc7552ac..dcb91613 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -63,6 +63,9 @@ protected: void doXBoxProfile(); Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void doEntitlements(); + Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void doMinecraftProfile(); Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); @@ -72,6 +75,8 @@ protected: void doGetSkin(); Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void succeed(); + void failResult(bool & flag); void succeedResult(bool & flag); void checkResult(); @@ -88,6 +93,7 @@ protected: int m_requestsDone = 0; bool m_xboxProfileSucceeded = false; bool m_mcAuthSucceeded = false; + QString entitlementsRequestId; QSet<int64_t> stsErrors; bool stsFailed = false; diff --git a/launcher/minecraft/auth/flows/Parsers.cpp b/launcher/minecraft/auth/flows/Parsers.cpp new file mode 100644 index 00000000..e79d67f2 --- /dev/null +++ b/launcher/minecraft/auth/flows/Parsers.cpp @@ -0,0 +1,315 @@ +#include "Parsers.h" + +#include <QJsonDocument> +#include <QJsonArray> + +namespace Parsers { + +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; +} + +bool getNumber(QJsonValue value, int64_t & out) { + if(!value.isDouble()) { + return false; + } + out = (int64_t) value.toDouble(); + return true; +} + +bool getBool(QJsonValue value, bool & out) { + if(!value.isBool()) { + return false; + } + out = value.toBool(); + 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, const char * name) { + qDebug() << "Parsing" << name <<":"; +#ifndef NDEBUG + qDebug() << data; +#endif + 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(); + return false; + } + + auto obj = doc.object(); + if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { + qWarning() << "User IssueInstant is not a timestamp"; + return false; + } + if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { + qWarning() << "User NotAfter is not a timestamp"; + return false; + } + if(!getString(obj.value("Token"), output.token)) { + qWarning() << "User Token is not a timestamp"; + return false; + } + auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); + if(!arrayVal.isArray()) { + qWarning() << "Missing xui claims array"; + 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..."; + return false; + } + output.extra[iter.key()] = claim; + } + + break; + } + if(!foundUHS) { + qWarning() << "Missing uhs"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << name << "is valid."; + return true; +} + +bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + 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(); + return false; + } + + auto obj = doc.object(); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + 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(); + + QString currentCape; + for(auto cape: capesArray) { + 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 = capeOut.id; + } + if(!getString(capeObj.value("url"), capeOut.url)) { + continue; + } + if(!getString(capeObj.value("alias"), capeOut.alias)) { + continue; + } + + output.capes[capeOut.id] = capeOut; + } + output.currentCape = currentCape; + output.validity = Katabasis::Validity::Certain; + return true; +} + +bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { + qDebug() << "Parsing Minecraft entitlements..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + 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(); + return false; + } + + auto obj = doc.object(); + + auto itemsArray = obj.value("items").toArray(); + for(auto item: itemsArray) { + auto itemObj = item.toObject(); + QString name; + if(!getString(itemObj.value("name"), name)) { + continue; + } + if(name == "game_minecraft") { + output.canPlayMinecraft = true; + } + if(name == "product_minecraft") { + output.ownsMinecraft = true; + } + } + output.validity = Katabasis::Validity::Certain; + return true; +} + +bool parseRolloutResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigration") { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + +bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { + QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); + 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"; + 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"; + return false; + } + + // TODO: it's a JWT... validate it? + if(!getString(obj.value("access_token"), output.token)) { + qWarning() << "access_token is not valid"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << "Mojang response is valid."; + return true; +} + +} diff --git a/launcher/minecraft/auth/flows/Parsers.h b/launcher/minecraft/auth/flows/Parsers.h new file mode 100644 index 00000000..6c6b64ee --- /dev/null +++ b/launcher/minecraft/auth/flows/Parsers.h @@ -0,0 +1,19 @@ +#pragma once + +#include "minecraft/auth/AccountData.h" + +namespace Parsers +{ + bool getDateTime(QJsonValue value, QDateTime & out); + bool getString(QJsonValue value, QString & out); + bool getNumber(QJsonValue value, double & out); + bool getNumber(QJsonValue value, int64_t & out); + bool getBool(QJsonValue value, bool & out); + + bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name); + bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); + + bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); + bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); + bool parseRolloutResponse(QByteArray &data, bool& result); +} |