aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth
diff options
context:
space:
mode:
authorTheCodex6824 <thecodex6824@gmail.com>2022-04-21 16:01:55 -0400
committerTheCodex6824 <thecodex6824@gmail.com>2022-04-22 23:39:38 -0400
commit8bcbe07c87ee4b776d9ba743bb598f22ee80dda0 (patch)
tree9f25883df7d42d8ec2db8d569b8ae27619e5da5d /launcher/minecraft/auth
parent5adcc26190b82dc9c1050452645d762f5e8b5a5e (diff)
downloadPrismLauncher-8bcbe07c87ee4b776d9ba743bb598f22ee80dda0.tar.gz
PrismLauncher-8bcbe07c87ee4b776d9ba743bb598f22ee80dda0.tar.bz2
PrismLauncher-8bcbe07c87ee4b776d9ba743bb598f22ee80dda0.zip
Fix Mojang auth failing due to Mojang rejecting requests to the profile endpoint
Diffstat (limited to 'launcher/minecraft/auth')
-rw-r--r--launcher/minecraft/auth/Parsers.cpp170
-rw-r--r--launcher/minecraft/auth/Parsers.h1
-rw-r--r--launcher/minecraft/auth/Yggdrasil.cpp22
-rw-r--r--launcher/minecraft/auth/flows/Mojang.cpp6
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp94
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h22
6 files changed, 312 insertions, 3 deletions
diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp
index 2dd36562..82f23559 100644
--- a/launcher/minecraft/auth/Parsers.cpp
+++ b/launcher/minecraft/auth/Parsers.cpp
@@ -212,6 +212,176 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
return true;
}
+namespace {
+ // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee)
+ // they are needed because the session server doesn't return skin urls for default skins
+ static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b";
+ static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032";
+
+ bool isDefaultModelSteve(QString uuid) {
+ // need to calculate *Java* hashCode of UUID
+ // if number is even, skin/model is steve, otherwise it is alex
+
+ // just in case dashes are in the id
+ uuid.remove('-');
+
+ if (uuid.size() != 32) {
+ return true;
+ }
+
+ // qulonglong is guaranteed to be 64 bits
+ // we need to use unsigned numbers to guarantee truncation below
+ qulonglong most = uuid.left(16).toULongLong(nullptr, 16);
+ qulonglong least = uuid.right(16).toULongLong(nullptr, 16);
+ qulonglong xored = most ^ least;
+ return ((static_cast<quint32>(xored >> 32)) ^ static_cast<quint32>(xored)) % 2 == 0;
+ }
+}
+
+/**
+Uses session server for skin/cape lookup instead of profile,
+because locked Mojang accounts cannot access profile endpoint
+(https://api.minecraftservices.com/minecraft/profile/)
+
+ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape
+
+{
+ "id": "<profile identifier>",
+ "name": "<player name>",
+ "properties": [
+ {
+ "name": "textures",
+ "value": "<base64 string>"
+ }
+ ]
+}
+
+decoded base64 "value":
+{
+ "timestamp": <java time in ms>,
+ "profileId": "<profile uuid>",
+ "profileName": "<player name>",
+ "textures": {
+ "SKIN": {
+ "url": "<player skin URL>"
+ },
+ "CAPE": {
+ "url": "<player cape URL>"
+ }
+ }
+}
+*/
+
+bool parseMinecraftProfileMojang(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 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 propsArray = obj.value("properties").toArray();
+ QByteArray texturePayload;
+ for( auto p : propsArray) {
+ auto pObj = p.toObject();
+ auto name = pObj.value("name");
+ if (!name.isString() || name.toString() != "textures") {
+ continue;
+ }
+
+ auto value = pObj.value("value");
+ if (value.isString()) {
+ texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors);
+ }
+
+ if (!texturePayload.isEmpty()) {
+ break;
+ }
+ }
+
+ if (texturePayload.isNull()) {
+ qWarning() << "No texture payload data";
+ return false;
+ }
+
+ doc = QJsonDocument::fromJson(texturePayload, &jsonError);
+ if(jsonError.error) {
+ qWarning() << "Failed to parse response as JSON: " << jsonError.errorString();
+ return false;
+ }
+
+ obj = doc.object();
+ auto textures = obj.value("textures");
+ if (!textures.isObject()) {
+ qWarning() << "No textures array in response";
+ return false;
+ }
+
+ Skin skinOut;
+ // fill in default skin info ourselves, as this endpoint doesn't provide it
+ bool steve = isDefaultModelSteve(output.id);
+ skinOut.variant = steve ? "classic" : "slim";
+ skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX;
+ // sadly we can't figure this out, but I don't think it really matters...
+ skinOut.id = "00000000-0000-0000-0000-000000000000";
+ Cape capeOut;
+ auto tObj = textures.toObject();
+ for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) {
+ if (idx->isObject()) {
+ if (idx.key() == "SKIN") {
+ auto skin = idx->toObject();
+ if (!getString(skin.value("url"), skinOut.url)) {
+ qWarning() << "Skin url is not a string";
+ return false;
+ }
+
+ auto maybeMeta = skin.find("metadata");
+ if (maybeMeta != skin.end() && maybeMeta->isObject()) {
+ auto meta = maybeMeta->toObject();
+ // might not be present
+ getString(meta.value("model"), skinOut.variant);
+ }
+ }
+ else if (idx.key() == "CAPE") {
+ auto cape = idx->toObject();
+ if (!getString(cape.value("url"), capeOut.url)) {
+ qWarning() << "Cape url is not a string";
+ return false;
+ }
+
+ // we don't know the cape ID as it is not returned from the session server
+ // so just fake it - changing capes is probably locked anyway :(
+ capeOut.alias = "cape";
+ }
+ }
+ }
+
+ output.skin = skinOut;
+ if (capeOut.alias == "cape") {
+ output.capes = QMap<QString, Cape>({{capeOut.alias, capeOut}});
+ output.currentCape = capeOut.alias;
+ }
+
+ output.validity = Katabasis::Validity::Certain;
+ return true;
+}
+
bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) {
qDebug() << "Parsing Minecraft entitlements...";
#ifndef NDEBUG
diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h
index dac7f69b..2666d890 100644
--- a/launcher/minecraft/auth/Parsers.h
+++ b/launcher/minecraft/auth/Parsers.h
@@ -14,6 +14,7 @@ namespace Parsers
bool parseMojangResponse(QByteArray &data, Katabasis::Token &output);
bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output);
+ bool parseMinecraftProfileMojang(QByteArray &data, MinecraftProfile &output);
bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output);
bool parseRolloutResponse(QByteArray &data, bool& result);
}
diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp
index 7ac842a6..29978411 100644
--- a/launcher/minecraft/auth/Yggdrasil.cpp
+++ b/launcher/minecraft/auth/Yggdrasil.cpp
@@ -209,6 +209,28 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
+ // Get UUID here since we need it for later
+ auto profile = responseData.value("selectedProfile");
+ if (!profile.isObject()) {
+ changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile."));
+ return;
+ }
+
+ auto profileObj = profile.toObject();
+ for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
+ if (i.key() == "name" && i.value().isString()) {
+ m_data->minecraftProfile.name = i->toString();
+ }
+ else if (i.key() == "id" && i.value().isString()) {
+ m_data->minecraftProfile.id = i->toString();
+ }
+ }
+
+ if (m_data->minecraftProfile.id.isEmpty()) {
+ changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
+ return;
+ }
+
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
qDebug() << "Finished reading authentication response.";
diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp
index 4661dbe2..b86b0936 100644
--- a/launcher/minecraft/auth/flows/Mojang.cpp
+++ b/launcher/minecraft/auth/flows/Mojang.cpp
@@ -1,7 +1,7 @@
#include "Mojang.h"
#include "minecraft/auth/steps/YggdrasilStep.h"
-#include "minecraft/auth/steps/MinecraftProfileStep.h"
+#include "minecraft/auth/steps/MinecraftProfileStepMojang.h"
#include "minecraft/auth/steps/MigrationEligibilityStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
@@ -10,7 +10,7 @@ MojangRefresh::MojangRefresh(
QObject *parent
) : AuthFlow(data, parent) {
m_steps.append(new YggdrasilStep(m_data, QString()));
- m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new MinecraftProfileStepMojang(m_data));
m_steps.append(new MigrationEligibilityStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}
@@ -21,7 +21,7 @@ MojangLogin::MojangLogin(
QObject *parent
): AuthFlow(data, parent), m_password(password) {
m_steps.append(new YggdrasilStep(m_data, m_password));
- m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new MinecraftProfileStepMojang(m_data));
m_steps.append(new MigrationEligibilityStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
new file mode 100644
index 00000000..d3035272
--- /dev/null
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
@@ -0,0 +1,94 @@
+#include "MinecraftProfileStepMojang.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {
+
+}
+
+MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default;
+
+QString MinecraftProfileStepMojang::describe() {
+ return tr("Fetching the Minecraft profile.");
+}
+
+
+void MinecraftProfileStepMojang::perform() {
+ if (m_data->minecraftProfile.id.isEmpty()) {
+ emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
+ return;
+ }
+
+ // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts
+ QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id);
+ QNetworkRequest req = QNetworkRequest(url);
+ AuthRequest *request = new AuthRequest(this);
+ connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone);
+ request->get(req);
+}
+
+void MinecraftProfileStepMojang::rehydrate() {
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void MinecraftProfileStepMojang::onRequestDone(
+ QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ if (error == QNetworkReply::ContentNotFoundError) {
+ // NOTE: Succeed even if we do not have a profile. This is a valid account state.
+ if(m_data->type == AccountType::Mojang) {
+ m_data->minecraftEntitlement.canPlayMinecraft = false;
+ m_data->minecraftEntitlement.ownsMinecraft = false;
+ }
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(
+ AccountTaskState::STATE_SUCCEEDED,
+ tr("Account has no Minecraft profile.")
+ );
+ return;
+ }
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Error getting profile:";
+ qWarning() << " HTTP Status: " << requestor->httpStatus_;
+ qWarning() << " Internal error no.: " << error;
+ qWarning() << " Error string: " << requestor->errorString_;
+
+ qWarning() << " Response:";
+ qWarning() << QString::fromUtf8(data);
+
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile acquisition failed.")
+ );
+ return;
+ }
+ if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile response could not be parsed")
+ );
+ return;
+ }
+
+ if(m_data->type == AccountType::Mojang) {
+ auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
+ m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
+ m_data->minecraftEntitlement.ownsMinecraft = validProfile;
+ }
+ emit finished(
+ AccountTaskState::STATE_WORKING,
+ tr("Minecraft Java profile acquisition succeeded.")
+ );
+}
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h
new file mode 100644
index 00000000..e06b30ab
--- /dev/null
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+
+class MinecraftProfileStepMojang : public AuthStep {
+ Q_OBJECT
+
+public:
+ explicit MinecraftProfileStepMojang(AccountData *data);
+ virtual ~MinecraftProfileStepMojang() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+};