aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/auth')
-rw-r--r--launcher/minecraft/auth/AccountData.cpp45
-rw-r--r--launcher/minecraft/auth/AccountData.h7
-rw-r--r--launcher/minecraft/auth/AccountList.cpp38
-rw-r--r--launcher/minecraft/auth/AccountList.h2
-rw-r--r--launcher/minecraft/auth/AuthSession.h1
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp17
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp343
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.h6
-rw-r--r--launcher/minecraft/auth/flows/Parsers.cpp315
-rw-r--r--launcher/minecraft/auth/flows/Parsers.h19
10 files changed, 476 insertions, 317 deletions
diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp
index 5c6de9df..8aa4e37f 100644
--- a/launcher/minecraft/auth/AccountData.cpp
+++ b/launcher/minecraft/auth/AccountData.cpp
@@ -207,6 +207,35 @@ MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * token
return out;
}
+void entitlementToJSONV3(QJsonObject &parent, MinecraftEntitlement p) {
+ if(p.validity == Katabasis::Validity::None) {
+ return;
+ }
+ QJsonObject out;
+ out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft);
+ out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft);
+ parent["entitlement"] = out;
+}
+
+bool entitlementFromJSONV3(const QJsonObject &parent, MinecraftEntitlement & out) {
+ auto entitlementObject = parent.value("entitlement").toObject();
+ if(entitlementObject.isEmpty()) {
+ return false;
+ }
+ {
+ auto ownsMinecraftV = entitlementObject.value("ownsMinecraft");
+ auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft");
+ if(!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) {
+ qWarning() << "mandatory attributes are missing or of unexpected type";
+ return false;
+ }
+ out.canPlayMinecraft = canPlayMinecraftV.toBool(false);
+ out.ownsMinecraft = ownsMinecraftV.toBool(false);
+ out.validity = Katabasis::Validity::Assumed;
+ }
+ return true;
+}
+
}
bool AccountData::resumeStateFromV2(QJsonObject data) {
@@ -304,9 +333,15 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
yggdrasilToken = tokenFromJSONV3(data, "ygg");
minecraftProfile = profileFromJSONV3(data, "profile");
+ if(!entitlementFromJSONV3(data, minecraftEntitlement)) {
+ if(minecraftProfile.validity != Katabasis::Validity::None) {
+ minecraftEntitlement.canPlayMinecraft = true;
+ minecraftEntitlement.ownsMinecraft = true;
+ minecraftEntitlement.validity = Katabasis::Validity::Assumed;
+ }
+ }
validity_ = minecraftProfile.validity;
-
return true;
}
@@ -331,6 +366,7 @@ QJsonObject AccountData::saveState() const {
tokenToJSONV3(output, yggdrasilToken, "ygg");
profileToJSONV3(output, minecraftProfile, "profile");
+ entitlementToJSONV3(output, minecraftEntitlement);
return output;
}
@@ -378,7 +414,12 @@ QString AccountData::profileId() const {
}
QString AccountData::profileName() const {
- return minecraftProfile.name;
+ if(minecraftProfile.name.size() == 0) {
+ return QObject::tr("No profile (%1)").arg(accountDisplayString());
+ }
+ else {
+ return minecraftProfile.name;
+ }
}
QString AccountData::accountDisplayString() const {
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h
index cf58fb76..09cd2c73 100644
--- a/launcher/minecraft/auth/AccountData.h
+++ b/launcher/minecraft/auth/AccountData.h
@@ -21,6 +21,12 @@ struct Cape {
QByteArray data;
};
+struct MinecraftEntitlement {
+ bool ownsMinecraft = false;
+ bool canPlayMinecraft = false;
+ Katabasis::Validity validity = Katabasis::Validity::None;
+};
+
struct MinecraftProfile {
QString id;
QString name;
@@ -69,5 +75,6 @@ struct AccountData {
Katabasis::Token yggdrasilToken;
MinecraftProfile minecraftProfile;
+ MinecraftEntitlement minecraftEntitlement;
Katabasis::Validity validity_ = Katabasis::Validity::None;
};
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index a76cac55..4895c39c 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -64,21 +64,18 @@ const MinecraftAccountPtr AccountList::at(int i) const
void AccountList::addAccount(const MinecraftAccountPtr account)
{
- // We only ever want accounts with valid profiles.
- // Keeping profile-less accounts is pointless and serves no purpose.
auto profileId = account->profileId();
- if(!profileId.size()) {
- return;
+ if(profileId.size()) {
+ // override/replace existing account with the same profileId
+ auto existingAccount = findAccountByProfileId(profileId);
+ if(existingAccount != -1) {
+ m_accounts[existingAccount] = account;
+ emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1));
+ onListChanged();
+ return;
+ }
}
- // override/replace existing account with the same profileId
- auto existingAccount = findAccountByProfileId(profileId);
- if(existingAccount != -1) {
- m_accounts[existingAccount] = account;
- emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1));
- onListChanged();
- return;
- }
// if we don't have this porfileId yet, add the account to the end
int row = m_accounts.count();
@@ -112,9 +109,9 @@ MinecraftAccountPtr AccountList::activeAccount() const
return m_activeAccount;
}
-void AccountList::setActiveAccount(const QString &profileId)
+void AccountList::setActiveAccount(MinecraftAccountPtr newAccount)
{
- if (profileId.isEmpty() && m_activeAccount)
+ if (!newAccount && m_activeAccount)
{
int idx = 0;
auto prevActiveAcc = m_activeAccount;
@@ -138,7 +135,7 @@ void AccountList::setActiveAccount(const QString &profileId)
int idx = 0;
for (MinecraftAccountPtr account : m_accounts)
{
- if (account->profileId() == profileId)
+ if (account == newAccount)
{
newActiveAccount = account;
newActiveAccountIdx = idx;
@@ -321,7 +318,7 @@ bool AccountList::setData(const QModelIndex &index, const QVariant &value, int r
if(value == Qt::Checked)
{
MinecraftAccountPtr account = at(index.row());
- setActiveAccount(account->profileId());
+ setActiveAccount(account);
}
}
@@ -435,11 +432,10 @@ bool AccountList::loadV3(QJsonObject& root) {
if (account.get() != nullptr)
{
auto profileId = account->profileId();
- if(!profileId.size()) {
- continue;
- }
- if(findAccountByProfileId(profileId) != -1) {
- continue;
+ if(profileId.size()) {
+ if(findAccountByProfileId(profileId) != -1) {
+ continue;
+ }
}
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
m_accounts.append(account);
diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h
index e275eb17..d6d72cd3 100644
--- a/launcher/minecraft/auth/AccountList.h
+++ b/launcher/minecraft/auth/AccountList.h
@@ -79,7 +79,7 @@ public:
bool saveList();
MinecraftAccountPtr activeAccount() const;
- void setActiveAccount(const QString &profileId);
+ void setActiveAccount(MinecraftAccountPtr profileId);
bool anyAccountIsValid();
signals:
diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h
index f609d5d3..f631a97d 100644
--- a/launcher/minecraft/auth/AuthSession.h
+++ b/launcher/minecraft/auth/AuthSession.h
@@ -17,6 +17,7 @@ struct AuthSession
Undetermined,
RequiresOAuth,
RequiresPassword,
+ RequiresProfileSetup,
PlayableOffline,
PlayableOnline,
GoneOrMigrated
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 2d76f9ac..8e294443 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -213,8 +213,21 @@ void MinecraftAccount::authSucceeded()
auto session = m_currentTask->getAssignedSession();
if (session)
{
- session->status =
- session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
+ /*
+ session->status = AuthSession::RequiresProfileSetup;
+ session->auth_server_online = true;
+ */
+ if(data.profileId().size() == 0) {
+ session->status = AuthSession::RequiresProfileSetup;
+ }
+ else {
+ if(session->wants_online) {
+ session->status = AuthSession::PlayableOnline;
+ }
+ else {
+ session->status = AuthSession::PlayableOffline;
+ }
+ }
fillSession(session);
session->auth_server_online = true;
}
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);
+}