GH-4071 handle network errors when logging in with MSA as 'soft'

This makes the tokens not expire when such errors happen.

Only applies to MSA, not the XBox and Mojang steps afterwards.
Further testing and improvements are still needed.
This commit is contained in:
Petr Mrázek
2021-11-28 18:42:01 +01:00
parent 0e31f77468
commit 285188ea53
17 changed files with 724 additions and 1218 deletions

View File

@ -244,8 +244,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
}
case StatusColumn: {
auto isActive = account->isActive();
return isActive ? "Working" : "Ready";
if(account->isActive()) {
return tr("Working", "Account status");
}
if(account->isExpired()) {
return tr("Expired", "Account status");
}
return tr("Ready", "Account status");
}
case ProfileNameColumn: {

View File

@ -34,6 +34,11 @@
#include "flows/MojangRefresh.h"
#include "flows/MojangLogin.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
}
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
MinecraftAccountPtr account(new MinecraftAccount());
if(account->data.resumeStateFromV2(json)) {
@ -52,7 +57,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
{
MinecraftAccountPtr account(new MinecraftAccount());
MinecraftAccountPtr account = new MinecraftAccount();
account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
@ -91,6 +96,23 @@ AccountStatus MinecraftAccount::accountStatus() const {
}
}
bool MinecraftAccount::isExpired() const {
switch(data.type) {
case AccountType::Mojang: {
return data.accessToken().isEmpty();
}
break;
case AccountType::MSA: {
return data.msaToken.validity == Katabasis::Validity::None;
}
break;
default: {
return true;
}
}
}
QPixmap MinecraftAccount::getFace() const {
QPixmap skinTexture;
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {

View File

@ -72,7 +72,7 @@ public: /* construction */
explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete;
//! Default constructor
explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {};
explicit MinecraftAccount(QObject *parent = 0);
static MinecraftAccountPtr createFromUsername(const QString &username);
@ -97,6 +97,10 @@ public: /* manipulation */
shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session);
public: /* queries */
QString internalId() const {
return m_internalId;
}
QString accountDisplayString() const {
return data.accountDisplayString();
}
@ -119,6 +123,8 @@ public: /* queries */
bool isActive() const;
bool isExpired() const;
bool canMigrate() const {
return data.canMigrateToMSA;
}
@ -168,6 +174,7 @@ signals:
// TODO: better signalling for the various possible state changes - especially errors
protected: /* variables */
QString m_internalId;
AccountData data;
// current task we are executing here

View File

@ -18,7 +18,7 @@
#include <Application.h>
using OAuth2 = Katabasis::OAuth2;
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
AuthContext::AuthContext(AccountData * data, QObject *parent) :
@ -50,21 +50,17 @@ void AuthContext::initMSA() {
return;
}
Katabasis::OAuth2::Options opts;
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = APPLICATION->msaClientId();
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
// FIXME: OAuth2 is not aware of our fancy shared pointers
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice);
connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
}
void AuthContext::initMojang() {
@ -78,7 +74,7 @@ void AuthContext::initMojang() {
}
void AuthContext::onMojangSucceeded() {
doEntitlements();
doMinecraftProfile();
}
@ -89,50 +85,56 @@ void AuthContext::onMojangFailed() {
changeState(m_yggdrasil->accountState(), tr("Mojang 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::onOAuthLinkingFailed() {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
}
void AuthContext::onOAuthLinkingSucceeded() {
emit hideVerificationUriAndCode();
auto *o2t = qobject_cast<OAuth2 *>(sender());
if (!o2t->linked()) {
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
return;
}
QVariantMap extraTokens = o2t->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
#endif
doUserAuth();
}
void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
// respond to activity change here
switch(activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::Refreshing:
case Katabasis::Activity::LoggingOut: {
// We asked it to do something, it's doing it. Nothing to act upon.
return;
}
case Katabasis::Activity::Succeeded: {
// Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode();
if (!m_oauth2->linked()) {
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
return;
}
QVariantMap extraTokens = m_oauth2->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
#endif
doUserAuth();
return;
}
case Katabasis::Activity::FailedSoft: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error."));
return;
}
case Katabasis::Activity::FailedGone:
case Katabasis::Activity::FailedHard: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
return;
}
default: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
return;
}
}
}
void AuthContext::doUserAuth() {
@ -226,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() {
void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
if(error == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
@ -543,6 +545,10 @@ void AuthContext::onMinecraftProfileDone(
#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();
succeed();
return;
@ -560,6 +566,9 @@ void AuthContext::onMinecraftProfileDone(
}
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;
doMigrationEligibilityCheck();
}
else {

View File

@ -7,7 +7,7 @@
#include <QNetworkReply>
#include <QImage>
#include <katabasis/OAuth2.h>
#include <katabasis/DeviceFlow.h>
#include "Yggdrasil.h"
#include "../AccountData.h"
#include "../AccountTask.h"
@ -35,9 +35,6 @@ signals:
private slots:
// OAuth-specific callbacks
void onOAuthLinkingSucceeded();
void onOAuthLinkingFailed();
void onOAuthActivityChanged(Katabasis::Activity activity);
// Yggdrasil specific callbacks
@ -87,7 +84,7 @@ protected:
void clearTokens();
protected:
Katabasis::OAuth2 *m_oauth2 = nullptr;
Katabasis::DeviceFlow *m_oauth2 = nullptr;
Yggdrasil *m_yggdrasil = nullptr;
int m_requestsDone = 0;

View File

@ -17,7 +17,6 @@ void MSAInteractive::executeTask() {
m_oauth2->setExtraRequestParams(extraOpts);
beginActivity(Katabasis::Activity::LoggingIn);
m_oauth2->unlink();
*m_data = AccountData();
m_oauth2->link();
m_oauth2->login();
}

View File

@ -19,7 +19,6 @@
#include <QDrag>
#include <QPainter>
#include "VersionListView.h"
#include "Common.h"
VersionListView::VersionListView(QWidget *parent)
:QTreeView ( parent )