Fix Mojang auth failing due to Mojang rejecting requests to the profile endpoint
This commit is contained in:
parent
5adcc26190
commit
8bcbe07c87
@ -235,6 +235,8 @@ set(MINECRAFT_SOURCES
|
|||||||
minecraft/auth/steps/MigrationEligibilityStep.h
|
minecraft/auth/steps/MigrationEligibilityStep.h
|
||||||
minecraft/auth/steps/MinecraftProfileStep.cpp
|
minecraft/auth/steps/MinecraftProfileStep.cpp
|
||||||
minecraft/auth/steps/MinecraftProfileStep.h
|
minecraft/auth/steps/MinecraftProfileStep.h
|
||||||
|
minecraft/auth/steps/MinecraftProfileStepMojang.cpp
|
||||||
|
minecraft/auth/steps/MinecraftProfileStepMojang.h
|
||||||
minecraft/auth/steps/MSAStep.cpp
|
minecraft/auth/steps/MSAStep.cpp
|
||||||
minecraft/auth/steps/MSAStep.h
|
minecraft/auth/steps/MSAStep.h
|
||||||
minecraft/auth/steps/XboxAuthorizationStep.cpp
|
minecraft/auth/steps/XboxAuthorizationStep.cpp
|
||||||
|
@ -212,6 +212,176 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
|
|||||||
return true;
|
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) {
|
bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) {
|
||||||
qDebug() << "Parsing Minecraft entitlements...";
|
qDebug() << "Parsing Minecraft entitlements...";
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
|
@ -14,6 +14,7 @@ namespace Parsers
|
|||||||
bool parseMojangResponse(QByteArray &data, Katabasis::Token &output);
|
bool parseMojangResponse(QByteArray &data, Katabasis::Token &output);
|
||||||
|
|
||||||
bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output);
|
bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output);
|
||||||
|
bool parseMinecraftProfileMojang(QByteArray &data, MinecraftProfile &output);
|
||||||
bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output);
|
bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output);
|
||||||
bool parseRolloutResponse(QByteArray &data, bool& result);
|
bool parseRolloutResponse(QByteArray &data, bool& result);
|
||||||
}
|
}
|
||||||
|
@ -209,6 +209,28 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
|
|||||||
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
|
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
|
||||||
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
|
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 made it through the minefield of possible errors. Return true to indicate that
|
||||||
// we've succeeded.
|
// we've succeeded.
|
||||||
qDebug() << "Finished reading authentication response.";
|
qDebug() << "Finished reading authentication response.";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#include "Mojang.h"
|
#include "Mojang.h"
|
||||||
|
|
||||||
#include "minecraft/auth/steps/YggdrasilStep.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/MigrationEligibilityStep.h"
|
||||||
#include "minecraft/auth/steps/GetSkinStep.h"
|
#include "minecraft/auth/steps/GetSkinStep.h"
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ MojangRefresh::MojangRefresh(
|
|||||||
QObject *parent
|
QObject *parent
|
||||||
) : AuthFlow(data, parent) {
|
) : AuthFlow(data, parent) {
|
||||||
m_steps.append(new YggdrasilStep(m_data, QString()));
|
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 MigrationEligibilityStep(m_data));
|
||||||
m_steps.append(new GetSkinStep(m_data));
|
m_steps.append(new GetSkinStep(m_data));
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ MojangLogin::MojangLogin(
|
|||||||
QObject *parent
|
QObject *parent
|
||||||
): AuthFlow(data, parent), m_password(password) {
|
): AuthFlow(data, parent), m_password(password) {
|
||||||
m_steps.append(new YggdrasilStep(m_data, m_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 MigrationEligibilityStep(m_data));
|
||||||
m_steps.append(new GetSkinStep(m_data));
|
m_steps.append(new GetSkinStep(m_data));
|
||||||
}
|
}
|
||||||
|
94
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
Normal file
94
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
Normal file
@ -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.")
|
||||||
|
);
|
||||||
|
}
|
22
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h
Normal file
22
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h
Normal file
@ -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>);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user