GH-3392 dirty initial MSA support that shares logic with Mojang flows
Both act as the first step of AuthContext.
This commit is contained in:
752
launcher/minecraft/auth/flows/AuthContext.cpp
Normal file
752
launcher/minecraft/auth/flows/AuthContext.cpp
Normal file
@ -0,0 +1,752 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QDesktopServices>
|
||||
#include <QMetaEnum>
|
||||
#include <QDebug>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#include "AuthContext.h"
|
||||
#include "katabasis/Globals.h"
|
||||
#include "katabasis/Requestor.h"
|
||||
#include "BuildConfig.h"
|
||||
|
||||
using OAuth2 = Katabasis::OAuth2;
|
||||
using Requestor = Katabasis::Requestor;
|
||||
using Activity = Katabasis::Activity;
|
||||
|
||||
AuthContext::AuthContext(AccountData * data, QObject *parent) :
|
||||
AccountTask(data, parent)
|
||||
{
|
||||
mgr = new QNetworkAccessManager(this);
|
||||
}
|
||||
|
||||
void AuthContext::beginActivity(Activity activity) {
|
||||
if(isBusy()) {
|
||||
throw 0;
|
||||
}
|
||||
m_activity = activity;
|
||||
changeState(STATE_WORKING, "Initializing");
|
||||
emit activityChanged(m_activity);
|
||||
}
|
||||
|
||||
void AuthContext::finishActivity() {
|
||||
if(!isBusy()) {
|
||||
throw 0;
|
||||
}
|
||||
m_activity = Katabasis::Activity::Idle;
|
||||
m_stage = MSAStage::Idle;
|
||||
m_data->validity_ = m_data->minecraftProfile.validity;
|
||||
emit activityChanged(m_activity);
|
||||
}
|
||||
|
||||
void AuthContext::initMSA() {
|
||||
if(m_oauth2) {
|
||||
return;
|
||||
}
|
||||
Katabasis::OAuth2::Options opts;
|
||||
opts.scope = "XboxLive.signin offline_access";
|
||||
opts.clientIdentifier = BuildConfig.MSA_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};
|
||||
|
||||
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr);
|
||||
|
||||
connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
|
||||
connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
|
||||
connect(m_oauth2, &OAuth2::openBrowser, this, &AuthContext::onOpenBrowser);
|
||||
connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser);
|
||||
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
|
||||
}
|
||||
|
||||
void AuthContext::initMojang() {
|
||||
if(m_yggdrasil) {
|
||||
return;
|
||||
}
|
||||
m_yggdrasil = new Yggdrasil(m_data, this);
|
||||
|
||||
connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
|
||||
connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
|
||||
}
|
||||
|
||||
void AuthContext::onMojangSucceeded() {
|
||||
doMinecraftProfile();
|
||||
}
|
||||
|
||||
|
||||
void AuthContext::onMojangFailed() {
|
||||
finishActivity();
|
||||
m_error = m_yggdrasil->m_error;
|
||||
m_aborted = m_yggdrasil->m_aborted;
|
||||
changeState(m_yggdrasil->accountState(), "Microsoft user authentication failed.");
|
||||
}
|
||||
|
||||
/*
|
||||
bool AuthContext::signOut() {
|
||||
if(isBusy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
beginActivity(Activity::LoggingOut);
|
||||
m_oauth2->unlink();
|
||||
m_account = AccountData();
|
||||
finishActivity();
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
|
||||
void AuthContext::onOpenBrowser(const QUrl &url) {
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void AuthContext::onCloseBrowser() {
|
||||
|
||||
}
|
||||
|
||||
void AuthContext::onOAuthLinkingFailed() {
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "Microsoft user authentication failed.");
|
||||
}
|
||||
|
||||
void AuthContext::onOAuthLinkingSucceeded() {
|
||||
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
||||
if (!o2t->linked()) {
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).");
|
||||
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 AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
|
||||
// respond to activity change here
|
||||
}
|
||||
|
||||
void AuthContext::doUserAuth() {
|
||||
m_stage = MSAStage::UserAuth;
|
||||
changeState(STATE_WORKING, "Starting user authentication");
|
||||
|
||||
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_data->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, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::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::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;
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"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 AuthContext::onUserAuthDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "XBox user authentication failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if(!parseXTokenResponse(replyData, temp)) {
|
||||
qWarning() << "Could not parse user authentication response...";
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "XBox user authentication response could not be understood.");
|
||||
return;
|
||||
}
|
||||
m_data->userToken = temp;
|
||||
|
||||
m_stage = MSAStage::XboxAuth;
|
||||
changeState(STATE_WORKING, "Starting XBox authentication");
|
||||
|
||||
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 AuthContext::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_data->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, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||
}
|
||||
|
||||
void AuthContext::onSTSAuthMinecraftDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
m_requestsDone ++;
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if(!parseXTokenResponse(replyData, temp)) {
|
||||
qWarning() << "Could not parse authorization response for access to mojang services...";
|
||||
m_requestsDone ++;
|
||||
return;
|
||||
}
|
||||
|
||||
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
|
||||
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||
qDebug() << replyData;
|
||||
m_requestsDone ++;
|
||||
return;
|
||||
}
|
||||
m_data->mojangservicesToken = temp;
|
||||
|
||||
doMinecraftAuth();
|
||||
}
|
||||
|
||||
void AuthContext::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_data->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, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||
}
|
||||
|
||||
void AuthContext::onSTSAuthGenericDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
m_requestsDone ++;
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if(!parseXTokenResponse(replyData, temp)) {
|
||||
qWarning() << "Could not parse authorization response for access to xbox API...";
|
||||
m_requestsDone ++;
|
||||
return;
|
||||
}
|
||||
|
||||
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
|
||||
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||
qDebug() << replyData;
|
||||
m_requestsDone ++;
|
||||
return;
|
||||
}
|
||||
m_data->xboxApiToken = temp;
|
||||
|
||||
doXBoxProfile();
|
||||
}
|
||||
|
||||
|
||||
void AuthContext::doMinecraftAuth() {
|
||||
QString mc_auth_template = R"XXX(
|
||||
{
|
||||
"identityToken": "XBL3.0 x=%1;%2"
|
||||
}
|
||||
)XXX";
|
||||
auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->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, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::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 AuthContext::onMinecraftAuthDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
m_requestsDone ++;
|
||||
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
qDebug() << replyData;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) {
|
||||
qWarning() << "Could not parse login_with_xbox response...";
|
||||
qDebug() << replyData;
|
||||
return;
|
||||
}
|
||||
m_mcAuthSucceeded = true;
|
||||
|
||||
checkResult();
|
||||
}
|
||||
|
||||
void AuthContext::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_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
|
||||
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone);
|
||||
requestor->get(request);
|
||||
qDebug() << "Getting Xbox profile...";
|
||||
}
|
||||
|
||||
void AuthContext::onXBoxProfileDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
m_requestsDone ++;
|
||||
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
qDebug() << replyData;
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "XBox profile: " << replyData;
|
||||
|
||||
m_xboxProfileSucceeded = true;
|
||||
checkResult();
|
||||
}
|
||||
|
||||
void AuthContext::checkResult() {
|
||||
if(m_requestsDone != 2) {
|
||||
return;
|
||||
}
|
||||
if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
|
||||
doMinecraftProfile();
|
||||
}
|
||||
else {
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "XBox and/or Mojang authentication steps did not succeed");
|
||||
}
|
||||
}
|
||||
|
||||
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 AuthContext::doMinecraftProfile() {
|
||||
m_stage = MSAStage::MinecraftProfile;
|
||||
changeState(STATE_WORKING, "Starting minecraft profile acquisition");
|
||||
|
||||
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_data->yggdrasilToken.token).toUtf8());
|
||||
|
||||
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone);
|
||||
requestor->get(request);
|
||||
}
|
||||
|
||||
void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
|
||||
qDebug() << data;
|
||||
if (error == QNetworkReply::ContentNotFoundError) {
|
||||
m_data->minecraftProfile = MinecraftProfile();
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "Account is missing a profile");
|
||||
return;
|
||||
}
|
||||
if (error != QNetworkReply::NoError) {
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "Profile acquisition failed");
|
||||
return;
|
||||
}
|
||||
if(!parseMinecraftProfile(data, m_data->minecraftProfile)) {
|
||||
m_data->minecraftProfile = MinecraftProfile();
|
||||
finishActivity();
|
||||
changeState(STATE_FAILED_HARD, "Profile response could not be parsed");
|
||||
return;
|
||||
}
|
||||
doGetSkin();
|
||||
}
|
||||
|
||||
void AuthContext::doGetSkin() {
|
||||
m_stage = MSAStage::Skin;
|
||||
changeState(STATE_WORKING, "Starting skin acquisition");
|
||||
|
||||
auto url = QUrl(m_data->minecraftProfile.skin.url);
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone);
|
||||
requestor->get(request);
|
||||
}
|
||||
|
||||
void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
|
||||
if (error == QNetworkReply::NoError) {
|
||||
m_data->minecraftProfile.skin.data = data;
|
||||
}
|
||||
m_data->validity_ = Katabasis::Validity::Certain;
|
||||
finishActivity();
|
||||
changeState(STATE_SUCCEEDED, "Finished whole chain");
|
||||
}
|
||||
|
||||
QString AuthContext::getStateMessage() const {
|
||||
switch (m_accountState)
|
||||
{
|
||||
case STATE_WORKING:
|
||||
switch(m_stage) {
|
||||
case MSAStage::Idle: {
|
||||
QString loginMessage = tr("Logging in as %1 user");
|
||||
if(m_data->type == AccountType::MSA) {
|
||||
return loginMessage.arg("Microsoft");
|
||||
}
|
||||
else {
|
||||
return loginMessage.arg("Mojang");
|
||||
}
|
||||
}
|
||||
case MSAStage::UserAuth:
|
||||
return tr("Logging in as XBox user");
|
||||
case MSAStage::XboxAuth:
|
||||
return tr("Logging in with XBox and Mojang services");
|
||||
case MSAStage::MinecraftProfile:
|
||||
return tr("Getting Minecraft profile");
|
||||
case MSAStage::Skin:
|
||||
return tr("Getting Minecraft skin");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return AccountTask::getStateMessage();
|
||||
}
|
||||
}
|
94
launcher/minecraft/auth/flows/AuthContext.h
Normal file
94
launcher/minecraft/auth/flows/AuthContext.h
Normal file
@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QNetworkReply>
|
||||
#include <QImage>
|
||||
|
||||
#include <katabasis/OAuth2.h>
|
||||
#include "Yggdrasil.h"
|
||||
#include "../AccountData.h"
|
||||
#include "../AccountTask.h"
|
||||
|
||||
class AuthContext : public AccountTask
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AuthContext(AccountData * data, QObject *parent = 0);
|
||||
|
||||
bool isBusy() {
|
||||
return m_activity != Katabasis::Activity::Idle;
|
||||
};
|
||||
Katabasis::Validity validity() {
|
||||
return m_data->validity_;
|
||||
};
|
||||
|
||||
//bool signOut();
|
||||
|
||||
QString getStateMessage() const override;
|
||||
|
||||
signals:
|
||||
void activityChanged(Katabasis::Activity activity);
|
||||
|
||||
private slots:
|
||||
// OAuth-specific callbacks
|
||||
void onOAuthLinkingSucceeded();
|
||||
void onOAuthLinkingFailed();
|
||||
void onOpenBrowser(const QUrl &url);
|
||||
void onCloseBrowser();
|
||||
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||
|
||||
// Yggdrasil specific callbacks
|
||||
void onMojangSucceeded();
|
||||
void onMojangFailed();
|
||||
|
||||
protected:
|
||||
void initMSA();
|
||||
void initMojang();
|
||||
|
||||
void doUserAuth();
|
||||
Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doSTSAuthMinecraft();
|
||||
Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void doMinecraftAuth();
|
||||
Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doSTSAuthGeneric();
|
||||
Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void doXBoxProfile();
|
||||
Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doMinecraftProfile();
|
||||
Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doGetSkin();
|
||||
Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void checkResult();
|
||||
|
||||
protected:
|
||||
void beginActivity(Katabasis::Activity activity);
|
||||
void finishActivity();
|
||||
void clearTokens();
|
||||
|
||||
protected:
|
||||
Katabasis::OAuth2 *m_oauth2 = nullptr;
|
||||
Yggdrasil *m_yggdrasil = nullptr;
|
||||
|
||||
int m_requestsDone = 0;
|
||||
bool m_xboxProfileSucceeded = false;
|
||||
bool m_mcAuthSucceeded = false;
|
||||
Katabasis::Activity m_activity = Katabasis::Activity::Idle;
|
||||
enum class MSAStage {
|
||||
Idle,
|
||||
UserAuth,
|
||||
XboxAuth,
|
||||
MinecraftProfile,
|
||||
Skin
|
||||
} m_stage = MSAStage::Idle;
|
||||
|
||||
QNetworkAccessManager *mgr = nullptr;
|
||||
};
|
@ -1,202 +0,0 @@
|
||||
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "AuthenticateTask.h"
|
||||
#include "../MojangAccount.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
|
||||
AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
|
||||
QObject *parent)
|
||||
: YggdrasilTask(account, parent), m_password(password)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject AuthenticateTask::getRequestContent() const
|
||||
{
|
||||
/*
|
||||
* {
|
||||
* "agent": { // optional
|
||||
* "name": "Minecraft", // So far this is the only encountered value
|
||||
* "version": 1 // This number might be increased
|
||||
* // by the vanilla client in the future
|
||||
* },
|
||||
* "username": "mojang account name", // Can be an email address or player name for
|
||||
// unmigrated accounts
|
||||
* "password": "mojang account password",
|
||||
* "clientToken": "client identifier" // optional
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
|
||||
{
|
||||
QJsonObject agent;
|
||||
// C++ makes string literals void* for some stupid reason, so we have to tell it
|
||||
// QString... Thanks Obama.
|
||||
agent.insert("name", QString("Minecraft"));
|
||||
agent.insert("version", 1);
|
||||
req.insert("agent", agent);
|
||||
}
|
||||
|
||||
req.insert("username", m_account->username());
|
||||
req.insert("password", m_password);
|
||||
req.insert("requestUser", true);
|
||||
|
||||
// If we already have a client token, give it to the server.
|
||||
// Otherwise, let the server give us one.
|
||||
|
||||
if(m_account->m_clientToken.isEmpty())
|
||||
{
|
||||
auto uuid = QUuid::createUuid();
|
||||
auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
|
||||
m_account->m_clientToken = uuidString;
|
||||
}
|
||||
req.insert("clientToken", m_account->m_clientToken);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
void AuthenticateTask::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
qDebug() << "Getting client token.";
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty())
|
||||
{
|
||||
// Fail if the server gave us an empty client token
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
||||
return;
|
||||
}
|
||||
if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
// Set the client token.
|
||||
m_account->m_clientToken = clientToken;
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty())
|
||||
{
|
||||
// Fail if the server didn't give us an access token.
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
// Set the access token.
|
||||
m_account->m_accessToken = accessToken;
|
||||
|
||||
// Now we load the list of available profiles.
|
||||
// Mojang hasn't yet implemented the profile system,
|
||||
// but we might as well support what's there so we
|
||||
// don't have trouble implementing it later.
|
||||
qDebug() << "Loading profile list.";
|
||||
QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
|
||||
QList<AccountProfile> loadedProfiles;
|
||||
for (auto iter : availableProfiles)
|
||||
{
|
||||
QJsonObject profile = iter.toObject();
|
||||
// Profiles are easy, we just need their ID and name.
|
||||
QString id = profile.value("id").toString("");
|
||||
QString name = profile.value("name").toString("");
|
||||
bool legacy = profile.value("legacy").toBool(false);
|
||||
|
||||
if (id.isEmpty() || name.isEmpty())
|
||||
{
|
||||
// This should never happen, but we might as well
|
||||
// warn about it if it does so we can debug it easily.
|
||||
// You never know when Mojang might do something truly derpy.
|
||||
qWarning() << "Found entry in available profiles list with missing ID or name "
|
||||
"field. Ignoring it.";
|
||||
}
|
||||
|
||||
// Now, add a new AccountProfile entry to the list.
|
||||
loadedProfiles.append({id, name, legacy});
|
||||
}
|
||||
// Put the list of profiles we loaded into the MojangAccount object.
|
||||
m_account->m_profiles = loadedProfiles;
|
||||
|
||||
// Finally, we set the current profile to the correct value. This is pretty simple.
|
||||
// We do need to make sure that the current profile that the server gave us
|
||||
// is actually in the available profiles list.
|
||||
// If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
|
||||
qDebug() << "Setting current profile.";
|
||||
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
|
||||
QString currentProfileId = currentProfile.value("id").toString("");
|
||||
if (currentProfileId.isEmpty())
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
|
||||
return;
|
||||
}
|
||||
if (!m_account->setCurrentProfile(currentProfileId))
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
|
||||
return;
|
||||
}
|
||||
|
||||
// this is what the vanilla launcher passes to the userProperties launch param
|
||||
if (responseData.contains("user"))
|
||||
{
|
||||
User u;
|
||||
auto obj = responseData.value("user").toObject();
|
||||
u.id = obj.value("id").toString();
|
||||
auto propArray = obj.value("properties").toArray();
|
||||
for (auto prop : propArray)
|
||||
{
|
||||
auto propTuple = prop.toObject();
|
||||
auto name = propTuple.value("name").toString();
|
||||
auto value = propTuple.value("value").toString();
|
||||
u.properties.insert(name, value);
|
||||
}
|
||||
m_account->m_user = u;
|
||||
}
|
||||
|
||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||
// we've succeeded.
|
||||
qDebug() << "Finished reading authentication response.";
|
||||
changeState(STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
QString AuthenticateTask::getEndpoint() const
|
||||
{
|
||||
return "authenticate";
|
||||
}
|
||||
|
||||
QString AuthenticateTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case STATE_SENDING_REQUEST:
|
||||
return tr("Authenticating: Sending request...");
|
||||
case STATE_PROCESSING_RESPONSE:
|
||||
return tr("Authenticating: Processing response...");
|
||||
default:
|
||||
return YggdrasilTask::getStateMessage();
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../YggdrasilTask.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
/**
|
||||
* The authenticate task takes a MojangAccount with no access token and password and attempts to
|
||||
* authenticate with Mojang's servers.
|
||||
* If successful, it will set the MojangAccount's access token.
|
||||
*/
|
||||
class AuthenticateTask : public YggdrasilTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
|
||||
|
||||
protected:
|
||||
virtual QJsonObject getRequestContent() const override;
|
||||
|
||||
virtual QString getEndpoint() const override;
|
||||
|
||||
virtual void processResponse(QJsonObject responseData) override;
|
||||
|
||||
virtual QString getStateMessage() const override;
|
||||
|
||||
private:
|
||||
QString m_password;
|
||||
};
|
51
launcher/minecraft/auth/flows/MSAHelper.txt
Normal file
51
launcher/minecraft/auth/flows/MSAHelper.txt
Normal file
@ -0,0 +1,51 @@
|
||||
class Helper : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Helper(MSAFlows * context) : QObject(), context_(context), msg_(QString()) {
|
||||
QFile tokenCache("usercache.dat");
|
||||
if(tokenCache.open(QIODevice::ReadOnly)) {
|
||||
context_->resumeFromState(tokenCache.readAll());
|
||||
}
|
||||
}
|
||||
|
||||
public slots:
|
||||
void run() {
|
||||
connect(context_, &MSAFlows::activityChanged, this, &Helper::onActivityChanged);
|
||||
context_->silentSignIn();
|
||||
}
|
||||
|
||||
void onFailed() {
|
||||
qDebug() << "Login failed";
|
||||
}
|
||||
|
||||
void onActivityChanged(Katabasis::Activity activity) {
|
||||
if(activity == Katabasis::Activity::Idle) {
|
||||
switch(context_->validity()) {
|
||||
case Katabasis::Validity::None: {
|
||||
// account is gone, remove it.
|
||||
QFile::remove("usercache.dat");
|
||||
}
|
||||
break;
|
||||
case Katabasis::Validity::Assumed: {
|
||||
// this is basically a soft-failed refresh. do nothing.
|
||||
}
|
||||
break;
|
||||
case Katabasis::Validity::Certain: {
|
||||
// stuff got refreshed / signed in. Save.
|
||||
auto data = context_->saveState();
|
||||
QSaveFile tokenCache("usercache.dat");
|
||||
if(tokenCache.open(QIODevice::WriteOnly)) {
|
||||
tokenCache.write(context_->saveState());
|
||||
tokenCache.commit();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
MSAFlows *context_;
|
||||
QString msg_;
|
||||
};
|
20
launcher/minecraft/auth/flows/MSAInteractive.cpp
Normal file
20
launcher/minecraft/auth/flows/MSAInteractive.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
#include "MSAInteractive.h"
|
||||
|
||||
MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
|
||||
|
||||
void MSAInteractive::executeTask() {
|
||||
m_requestsDone = 0;
|
||||
m_xboxProfileSucceeded = false;
|
||||
m_mcAuthSucceeded = false;
|
||||
|
||||
initMSA();
|
||||
|
||||
QVariantMap extraOpts;
|
||||
extraOpts["prompt"] = "select_account";
|
||||
m_oauth2->setExtraRequestParams(extraOpts);
|
||||
|
||||
beginActivity(Katabasis::Activity::LoggingIn);
|
||||
m_oauth2->unlink();
|
||||
*m_data = AccountData();
|
||||
m_oauth2->link();
|
||||
}
|
10
launcher/minecraft/auth/flows/MSAInteractive.h
Normal file
10
launcher/minecraft/auth/flows/MSAInteractive.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "AuthContext.h"
|
||||
|
||||
class MSAInteractive : public AuthContext
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MSAInteractive(AccountData * data, QObject *parent = 0);
|
||||
void executeTask() override;
|
||||
};
|
16
launcher/minecraft/auth/flows/MSASilent.cpp
Normal file
16
launcher/minecraft/auth/flows/MSASilent.cpp
Normal file
@ -0,0 +1,16 @@
|
||||
#include "MSASilent.h"
|
||||
|
||||
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
|
||||
|
||||
void MSASilent::executeTask() {
|
||||
m_requestsDone = 0;
|
||||
m_xboxProfileSucceeded = false;
|
||||
m_mcAuthSucceeded = false;
|
||||
|
||||
initMSA();
|
||||
|
||||
beginActivity(Katabasis::Activity::Refreshing);
|
||||
if(!m_oauth2->refresh()) {
|
||||
finishActivity();
|
||||
}
|
||||
}
|
10
launcher/minecraft/auth/flows/MSASilent.h
Normal file
10
launcher/minecraft/auth/flows/MSASilent.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "AuthContext.h"
|
||||
|
||||
class MSASilent : public AuthContext
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MSASilent(AccountData * data, QObject *parent = 0);
|
||||
void executeTask() override;
|
||||
};
|
14
launcher/minecraft/auth/flows/MojangLogin.cpp
Normal file
14
launcher/minecraft/auth/flows/MojangLogin.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
#include "MojangLogin.h"
|
||||
|
||||
MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthContext(data, parent), m_password(password) {}
|
||||
|
||||
void MojangLogin::executeTask() {
|
||||
m_requestsDone = 0;
|
||||
m_xboxProfileSucceeded = false;
|
||||
m_mcAuthSucceeded = false;
|
||||
|
||||
initMojang();
|
||||
|
||||
beginActivity(Katabasis::Activity::LoggingIn);
|
||||
m_yggdrasil->login(m_password);
|
||||
}
|
13
launcher/minecraft/auth/flows/MojangLogin.h
Normal file
13
launcher/minecraft/auth/flows/MojangLogin.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "AuthContext.h"
|
||||
|
||||
class MojangLogin : public AuthContext
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MojangLogin(AccountData * data, QString password, QObject *parent = 0);
|
||||
void executeTask() override;
|
||||
|
||||
private:
|
||||
QString m_password;
|
||||
};
|
14
launcher/minecraft/auth/flows/MojangRefresh.cpp
Normal file
14
launcher/minecraft/auth/flows/MojangRefresh.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
#include "MojangRefresh.h"
|
||||
|
||||
MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
|
||||
|
||||
void MojangRefresh::executeTask() {
|
||||
m_requestsDone = 0;
|
||||
m_xboxProfileSucceeded = false;
|
||||
m_mcAuthSucceeded = false;
|
||||
|
||||
initMojang();
|
||||
|
||||
beginActivity(Katabasis::Activity::Refreshing);
|
||||
m_yggdrasil->refresh();
|
||||
}
|
10
launcher/minecraft/auth/flows/MojangRefresh.h
Normal file
10
launcher/minecraft/auth/flows/MojangRefresh.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "AuthContext.h"
|
||||
|
||||
class MojangRefresh : public AuthContext
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MojangRefresh(AccountData * data, QObject *parent = 0);
|
||||
void executeTask() override;
|
||||
};
|
@ -1,144 +0,0 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "RefreshTask.h"
|
||||
#include "../MojangAccount.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject RefreshTask::getRequestContent() const
|
||||
{
|
||||
/*
|
||||
* {
|
||||
* "clientToken": "client identifier"
|
||||
* "accessToken": "current access token to be refreshed"
|
||||
* "selectedProfile": // specifying this causes errors
|
||||
* {
|
||||
* "id": "profile ID"
|
||||
* "name": "profile name"
|
||||
* }
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
req.insert("clientToken", m_account->m_clientToken);
|
||||
req.insert("accessToken", m_account->m_accessToken);
|
||||
/*
|
||||
{
|
||||
auto currentProfile = m_account->currentProfile();
|
||||
QJsonObject profile;
|
||||
profile.insert("id", currentProfile->id());
|
||||
profile.insert("name", currentProfile->name());
|
||||
req.insert("selectedProfile", profile);
|
||||
}
|
||||
*/
|
||||
req.insert("requestUser", true);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
void RefreshTask::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty())
|
||||
{
|
||||
// Fail if the server gave us an empty client token
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
||||
return;
|
||||
}
|
||||
if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting new access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty())
|
||||
{
|
||||
// Fail if the server didn't give us an access token.
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
|
||||
// we validate that the server responded right. (our current profile = returned current
|
||||
// profile)
|
||||
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
|
||||
QString currentProfileId = currentProfile.value("id").toString("");
|
||||
if (m_account->currentProfile()->id != currentProfileId)
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
|
||||
return;
|
||||
}
|
||||
|
||||
// this is what the vanilla launcher passes to the userProperties launch param
|
||||
if (responseData.contains("user"))
|
||||
{
|
||||
User u;
|
||||
auto obj = responseData.value("user").toObject();
|
||||
u.id = obj.value("id").toString();
|
||||
auto propArray = obj.value("properties").toArray();
|
||||
for (auto prop : propArray)
|
||||
{
|
||||
auto propTuple = prop.toObject();
|
||||
auto name = propTuple.value("name").toString();
|
||||
auto value = propTuple.value("value").toString();
|
||||
u.properties.insert(name, value);
|
||||
}
|
||||
m_account->m_user = u;
|
||||
}
|
||||
|
||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||
// we've succeeded.
|
||||
qDebug() << "Finished reading refresh response.";
|
||||
// Reset the access token.
|
||||
m_account->m_accessToken = accessToken;
|
||||
changeState(STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
QString RefreshTask::getEndpoint() const
|
||||
{
|
||||
return "refresh";
|
||||
}
|
||||
|
||||
QString RefreshTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case STATE_SENDING_REQUEST:
|
||||
return tr("Refreshing login token...");
|
||||
case STATE_PROCESSING_RESPONSE:
|
||||
return tr("Refreshing login token: Processing response...");
|
||||
default:
|
||||
return YggdrasilTask::getStateMessage();
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../YggdrasilTask.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
/**
|
||||
* The authenticate task takes a MojangAccount with a possibly timed-out access token
|
||||
* and attempts to authenticate with Mojang's servers.
|
||||
* If successful, it will set the new access token. The token is considered validated.
|
||||
*/
|
||||
class RefreshTask : public YggdrasilTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RefreshTask(MojangAccount * account);
|
||||
|
||||
protected:
|
||||
virtual QJsonObject getRequestContent() const override;
|
||||
|
||||
virtual QString getEndpoint() const override;
|
||||
|
||||
virtual void processResponse(QJsonObject responseData) override;
|
||||
|
||||
virtual QString getStateMessage() const override;
|
||||
};
|
||||
|
@ -1,61 +0,0 @@
|
||||
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ValidateTask.h"
|
||||
#include "../MojangAccount.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
|
||||
: YggdrasilTask(account, parent)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject ValidateTask::getRequestContent() const
|
||||
{
|
||||
QJsonObject req;
|
||||
req.insert("accessToken", m_account->m_accessToken);
|
||||
return req;
|
||||
}
|
||||
|
||||
void ValidateTask::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Assume that if processError wasn't called, then the request was successful.
|
||||
changeState(YggdrasilTask::STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
QString ValidateTask::getEndpoint() const
|
||||
{
|
||||
return "validate";
|
||||
}
|
||||
|
||||
QString ValidateTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case YggdrasilTask::STATE_SENDING_REQUEST:
|
||||
return tr("Validating access token: Sending request...");
|
||||
case YggdrasilTask::STATE_PROCESSING_RESPONSE:
|
||||
return tr("Validating access token: Processing response...");
|
||||
default:
|
||||
return YggdrasilTask::getStateMessage();
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../YggdrasilTask.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
/**
|
||||
* The validate task takes a MojangAccount and checks to make sure its access token is valid.
|
||||
*/
|
||||
class ValidateTask : public YggdrasilTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ValidateTask(MojangAccount *account, QObject *parent = 0);
|
||||
|
||||
protected:
|
||||
virtual QJsonObject getRequestContent() const override;
|
||||
|
||||
virtual QString getEndpoint() const override;
|
||||
|
||||
virtual void processResponse(QJsonObject responseData) override;
|
||||
|
||||
virtual QString getStateMessage() const override;
|
||||
|
||||
private:
|
||||
};
|
337
launcher/minecraft/auth/flows/Yggdrasil.cpp
Normal file
337
launcher/minecraft/auth/flows/Yggdrasil.cpp
Normal file
@ -0,0 +1,337 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "Yggdrasil.h"
|
||||
#include "../AccountData.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QByteArray>
|
||||
|
||||
#include <Env.h>
|
||||
|
||||
#include <BuildConfig.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
|
||||
: AccountTask(data, parent)
|
||||
{
|
||||
changeState(STATE_CREATED);
|
||||
}
|
||||
|
||||
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
|
||||
changeState(STATE_WORKING);
|
||||
|
||||
QNetworkRequest netRequest(endpoint);
|
||||
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
m_netReply = ENV.qnam().post(netRequest, content);
|
||||
connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
|
||||
connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
|
||||
connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
|
||||
connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
|
||||
timeout_keeper.setSingleShot(true);
|
||||
timeout_keeper.start(timeout_max);
|
||||
counter.setSingleShot(false);
|
||||
counter.start(time_step);
|
||||
progress(0, timeout_max);
|
||||
connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout);
|
||||
connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
|
||||
}
|
||||
|
||||
void Yggdrasil::executeTask() {
|
||||
}
|
||||
|
||||
void Yggdrasil::refresh() {
|
||||
start();
|
||||
/*
|
||||
* {
|
||||
* "clientToken": "client identifier"
|
||||
* "accessToken": "current access token to be refreshed"
|
||||
* "selectedProfile": // specifying this causes errors
|
||||
* {
|
||||
* "id": "profile ID"
|
||||
* "name": "profile name"
|
||||
* }
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
req.insert("clientToken", m_data->clientToken());
|
||||
req.insert("accessToken", m_data->accessToken());
|
||||
/*
|
||||
{
|
||||
auto currentProfile = m_account->currentProfile();
|
||||
QJsonObject profile;
|
||||
profile.insert("id", currentProfile->id());
|
||||
profile.insert("name", currentProfile->name());
|
||||
req.insert("selectedProfile", profile);
|
||||
}
|
||||
*/
|
||||
req.insert("requestUser", false);
|
||||
QJsonDocument doc(req);
|
||||
|
||||
QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh");
|
||||
QByteArray requestData = doc.toJson();
|
||||
|
||||
sendRequest(reqUrl, requestData);
|
||||
}
|
||||
|
||||
void Yggdrasil::login(QString password) {
|
||||
start();
|
||||
/*
|
||||
* {
|
||||
* "agent": { // optional
|
||||
* "name": "Minecraft", // So far this is the only encountered value
|
||||
* "version": 1 // This number might be increased
|
||||
* // by the vanilla client in the future
|
||||
* },
|
||||
* "username": "mojang account name", // Can be an email address or player name for
|
||||
* // unmigrated accounts
|
||||
* "password": "mojang account password",
|
||||
* "clientToken": "client identifier", // optional
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
|
||||
{
|
||||
QJsonObject agent;
|
||||
// C++ makes string literals void* for some stupid reason, so we have to tell it
|
||||
// QString... Thanks Obama.
|
||||
agent.insert("name", QString("Minecraft"));
|
||||
agent.insert("version", 1);
|
||||
req.insert("agent", agent);
|
||||
}
|
||||
|
||||
req.insert("username", m_data->userName());
|
||||
req.insert("password", password);
|
||||
req.insert("requestUser", false);
|
||||
|
||||
// If we already have a client token, give it to the server.
|
||||
// Otherwise, let the server give us one.
|
||||
|
||||
m_data->generateClientTokenIfMissing();
|
||||
req.insert("clientToken", m_data->clientToken());
|
||||
|
||||
QJsonDocument doc(req);
|
||||
|
||||
QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate");
|
||||
QNetworkRequest netRequest(reqUrl);
|
||||
QByteArray requestData = doc.toJson();
|
||||
|
||||
sendRequest(reqUrl, requestData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Yggdrasil::refreshTimers(qint64, qint64)
|
||||
{
|
||||
timeout_keeper.stop();
|
||||
timeout_keeper.start(timeout_max);
|
||||
progress(count = 0, timeout_max);
|
||||
}
|
||||
void Yggdrasil::heartbeat()
|
||||
{
|
||||
count += time_step;
|
||||
progress(count, timeout_max);
|
||||
}
|
||||
|
||||
bool Yggdrasil::abort()
|
||||
{
|
||||
progress(timeout_max, timeout_max);
|
||||
// TODO: actually use this in a meaningful way
|
||||
m_aborted = Yggdrasil::BY_USER;
|
||||
m_netReply->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Yggdrasil::abortByTimeout()
|
||||
{
|
||||
progress(timeout_max, timeout_max);
|
||||
// TODO: actually use this in a meaningful way
|
||||
m_aborted = Yggdrasil::BY_TIMEOUT;
|
||||
m_netReply->abort();
|
||||
}
|
||||
|
||||
void Yggdrasil::sslErrors(QList<QSslError> errors)
|
||||
{
|
||||
int i = 1;
|
||||
for (auto error : errors)
|
||||
{
|
||||
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
|
||||
auto cert = error.certificate();
|
||||
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void Yggdrasil::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty())
|
||||
{
|
||||
// Fail if the server gave us an empty client token
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
||||
return;
|
||||
}
|
||||
if(m_data->clientToken().isEmpty()) {
|
||||
m_data->setClientToken(clientToken);
|
||||
}
|
||||
else if(clientToken != m_data->clientToken()) {
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty())
|
||||
{
|
||||
// Fail if the server didn't give us an access token.
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
// Set the access token.
|
||||
m_data->yggdrasilToken.token = accessToken;
|
||||
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
|
||||
|
||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||
// we've succeeded.
|
||||
qDebug() << "Finished reading authentication response.";
|
||||
changeState(STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
void Yggdrasil::processReply()
|
||||
{
|
||||
changeState(STATE_WORKING);
|
||||
|
||||
switch (m_netReply->error())
|
||||
{
|
||||
case QNetworkReply::NoError:
|
||||
break;
|
||||
case QNetworkReply::TimeoutError:
|
||||
changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
|
||||
return;
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
|
||||
return;
|
||||
case QNetworkReply::SslHandshakeFailedError:
|
||||
changeState(
|
||||
STATE_FAILED_SOFT,
|
||||
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
|
||||
"<ul>"
|
||||
"<li>You use Windows XP and need to <a "
|
||||
"href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
|
||||
"your root certificates</a></li>"
|
||||
"<li>Some device on your network is interfering with SSL traffic. In that case, "
|
||||
"you have bigger worries than Minecraft not starting.</li>"
|
||||
"<li>Possibly something else. Check the MultiMC log file for details</li>"
|
||||
"</ul>"));
|
||||
return;
|
||||
// used for invalid credentials and similar errors. Fall through.
|
||||
case QNetworkReply::ContentAccessDenied:
|
||||
case QNetworkReply::ContentOperationNotPermittedError:
|
||||
break;
|
||||
default:
|
||||
changeState(STATE_FAILED_SOFT,
|
||||
tr("Authentication operation failed due to a network error: %1 (%2)")
|
||||
.arg(m_netReply->errorString()).arg(m_netReply->error()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse the response regardless of the response code.
|
||||
// Sometimes the auth server will give more information and an error code.
|
||||
QJsonParseError jsonError;
|
||||
QByteArray replyData = m_netReply->readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
|
||||
// Check the response code.
|
||||
int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (responseCode == 200)
|
||||
{
|
||||
// If the response code was 200, then there shouldn't be an error. Make sure
|
||||
// anyways.
|
||||
// Also, sometimes an empty reply indicates success. If there was no data received,
|
||||
// pass an empty json object to the processResponse function.
|
||||
if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
|
||||
{
|
||||
processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response "
|
||||
"JSON response: %1 at offset %2.")
|
||||
.arg(jsonError.errorString())
|
||||
.arg(jsonError.offset));
|
||||
qCritical() << replyData;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the response code was not 200, then Yggdrasil may have given us information
|
||||
// about the error.
|
||||
// If we can parse the response, then get information from it. Otherwise just say
|
||||
// there was an unknown error.
|
||||
if (jsonError.error == QJsonParseError::NoError)
|
||||
{
|
||||
// We were able to parse the server's response. Woo!
|
||||
// Call processError. If a subclass has overridden it then they'll handle their
|
||||
// stuff there.
|
||||
qDebug() << "The request failed, but the server gave us an error message. "
|
||||
"Processing error.";
|
||||
processError(doc.object());
|
||||
}
|
||||
else
|
||||
{
|
||||
// The server didn't say anything regarding the error. Give the user an unknown
|
||||
// error.
|
||||
qDebug()
|
||||
<< "The request failed and the server gave no error message. Unknown error.";
|
||||
changeState(STATE_FAILED_SOFT,
|
||||
tr("An unknown error occurred when trying to communicate with the "
|
||||
"authentication server: %1").arg(m_netReply->errorString()));
|
||||
}
|
||||
}
|
||||
|
||||
void Yggdrasil::processError(QJsonObject responseData)
|
||||
{
|
||||
QJsonValue errorVal = responseData.value("error");
|
||||
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
||||
QJsonValue causeVal = responseData.value("cause");
|
||||
|
||||
if (errorVal.isString() && errorMessageValue.isString())
|
||||
{
|
||||
m_error = std::shared_ptr<Error>(new Error{
|
||||
errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")});
|
||||
changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Error is not in standard format. Don't set m_error and return unknown error.
|
||||
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
||||
}
|
||||
}
|
82
launcher/minecraft/auth/flows/Yggdrasil.h
Normal file
82
launcher/minecraft/auth/flows/Yggdrasil.h
Normal file
@ -0,0 +1,82 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../AccountTask.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
#include <qsslerror.h>
|
||||
|
||||
#include "../MinecraftAccount.h"
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
/**
|
||||
* A Yggdrasil task is a task that performs an operation on a given mojang account.
|
||||
*/
|
||||
class Yggdrasil : public AccountTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Yggdrasil(AccountData * data, QObject *parent = 0);
|
||||
virtual ~Yggdrasil() {};
|
||||
|
||||
void refresh();
|
||||
void login(QString password);
|
||||
protected:
|
||||
void executeTask() override;
|
||||
|
||||
/**
|
||||
* Processes the response received from the server.
|
||||
* If an error occurred, this should emit a failed signal.
|
||||
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
|
||||
* Otherwise, it should return true.
|
||||
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
|
||||
* an empty QJsonObject.
|
||||
*/
|
||||
void processResponse(QJsonObject responseData);
|
||||
|
||||
/**
|
||||
* Processes an error response received from the server.
|
||||
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
|
||||
* \returns a QString error message that will be passed to emitFailed.
|
||||
*/
|
||||
virtual void processError(QJsonObject responseData);
|
||||
|
||||
protected slots:
|
||||
void processReply();
|
||||
void refreshTimers(qint64, qint64);
|
||||
void heartbeat();
|
||||
void sslErrors(QList<QSslError>);
|
||||
void abortByTimeout();
|
||||
|
||||
public slots:
|
||||
virtual bool abort() override;
|
||||
|
||||
private:
|
||||
void sendRequest(QUrl endpoint, QByteArray content);
|
||||
|
||||
protected:
|
||||
QNetworkReply *m_netReply = nullptr;
|
||||
QTimer timeout_keeper;
|
||||
QTimer counter;
|
||||
int count = 0; // num msec since time reset
|
||||
|
||||
const int timeout_max = 30000;
|
||||
const int time_step = 50;
|
||||
};
|
Reference in New Issue
Block a user