From eae65da110bb957194b34e0f3573ce4e6e6ddc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 22 Aug 2021 20:01:18 +0200 Subject: [PATCH] GH-3392 Switch MS account login to use device flow instead Device flow involves the user manually opening a web page and putting in a code. We no longer need to interact with the browser. --- launcher/dialogs/MSALoginDialog.cpp | 34 ++++++++++++++ launcher/dialogs/MSALoginDialog.h | 10 +++- launcher/dialogs/MSALoginDialog.ui | 3 ++ launcher/minecraft/auth/AccountTask.h | 4 ++ launcher/minecraft/auth/flows/AuthContext.cpp | 46 ++++++++++--------- launcher/minecraft/auth/flows/AuthContext.h | 14 +++--- .../katabasis/include/katabasis/OAuth2.h | 4 +- libraries/katabasis/src/OAuth2.cpp | 23 +++++----- 8 files changed, 96 insertions(+), 42 deletions(-) diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 14a0e243..989a4c9f 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -41,6 +41,9 @@ int MSALoginDialog::exec() { connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress); + connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode); + connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode); + connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); m_loginTask->start(); return QDialog::exec(); @@ -52,6 +55,37 @@ MSALoginDialog::~MSALoginDialog() delete ui; } +void MSALoginDialog::externalLoginTick() { + m_externalLoginElapsed++; + ui->progressBar->setValue(m_externalLoginElapsed); + ui->progressBar->repaint(); + + if(m_externalLoginElapsed >= m_externalLoginTimeout) { + m_externalLoginTimer.stop(); + } +} + + +void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) { + m_externalLoginElapsed = 0; + m_externalLoginTimeout = expiresIn; + + m_externalLoginTimer.setInterval(1000); + m_externalLoginTimer.setSingleShot(false); + m_externalLoginTimer.start(); + + ui->progressBar->setMaximum(expiresIn); + ui->progressBar->setValue(m_externalLoginElapsed); + + QString urlString = uri.toString(); + QString linkString = QString("%2").arg(urlString, urlString); + ui->label->setText(tr("

Please open up %1 in a browser and put in the code %2 to proceed with login.

").arg(linkString, code)); +} + +void MSALoginDialog::hideVerificationUriAndCode() { + m_externalLoginTimer.stop(); +} + void MSALoginDialog::setUserInputsEnabled(bool enable) { ui->buttonBox->setEnabled(enable); diff --git a/launcher/dialogs/MSALoginDialog.h b/launcher/dialogs/MSALoginDialog.h index 402180ee..3d26a0dd 100644 --- a/launcher/dialogs/MSALoginDialog.h +++ b/launcher/dialogs/MSALoginDialog.h @@ -17,6 +17,7 @@ #include #include +#include #include "minecraft/auth/MinecraftAccount.h" @@ -46,10 +47,17 @@ slots: void onTaskSucceeded(); void onTaskStatus(const QString &status); void onTaskProgress(qint64 current, qint64 total); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + + void externalLoginTick(); private: Ui::MSALoginDialog *ui; MinecraftAccountPtr m_account; - std::shared_ptr m_loginTask; + std::shared_ptr m_loginTask; + QTimer m_externalLoginTimer; + int m_externalLoginElapsed = 0; + int m_externalLoginTimeout = 0; }; diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui index 4d82fe2b..78cbfb26 100644 --- a/launcher/dialogs/MSALoginDialog.ui +++ b/launcher/dialogs/MSALoginDialog.ui @@ -30,6 +30,9 @@ aaaaa Qt::RichText + + true + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index 3f08096f..fc3488eb 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -83,6 +83,10 @@ public: return m_accountState; } +signals: + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + protected: /** diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9754d1a9..ecd7e310 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -43,7 +43,7 @@ void AuthContext::finishActivity() { throw 0; } m_activity = Katabasis::Activity::Idle; - m_stage = MSAStage::Idle; + setStage(AuthStage::Complete); m_data->validity_ = m_data->minecraftProfile.validity; emit activityChanged(m_activity); } @@ -55,16 +55,16 @@ void AuthContext::initMSA() { 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.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}; m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr); + 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::openBrowser, this, &AuthContext::onOpenBrowser); - connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); } @@ -106,20 +106,14 @@ bool AuthContext::signOut() { } */ -void AuthContext::onOpenBrowser(const QUrl &url) { - QDesktopServices::openUrl(url); -} - -void AuthContext::onCloseBrowser() { - -} - void AuthContext::onOAuthLinkingFailed() { + emit hideVerificationUriAndCode(); finishActivity(); changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); } void AuthContext::onOAuthLinkingSucceeded() { + emit hideVerificationUriAndCode(); auto *o2t = qobject_cast(sender()); if (!o2t->linked()) { finishActivity(); @@ -143,7 +137,7 @@ void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { } void AuthContext::doUserAuth() { - m_stage = MSAStage::UserAuth; + setStage(AuthStage::UserAuth); changeState(STATE_WORKING, tr("Starting user authentication")); QString xbox_auth_template = R"XXX( @@ -307,7 +301,7 @@ void AuthContext::onUserAuthDone( } m_data->userToken = temp; - m_stage = MSAStage::XboxAuth; + setStage(AuthStage::XboxAuth); changeState(STATE_WORKING, tr("Starting XBox authentication")); doSTSAuthMinecraft(); @@ -671,7 +665,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { } void AuthContext::doMinecraftProfile() { - m_stage = MSAStage::MinecraftProfile; + setStage(AuthStage::MinecraftProfile); changeState(STATE_WORKING, tr("Starting minecraft profile acquisition")); auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); @@ -710,7 +704,7 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, } void AuthContext::doGetSkin() { - m_stage = MSAStage::Skin; + setStage(AuthStage::Skin); changeState(STATE_WORKING, tr("Fetching player skin")); auto url = QUrl(m_data->minecraftProfile.skin.url); @@ -730,12 +724,18 @@ void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); } +void AuthContext::setStage(AuthContext::AuthStage stage) { + m_stage = stage; + emit progress((int)m_stage, (int)AuthStage::Complete); +} + + QString AuthContext::getStateMessage() const { switch (m_accountState) { case STATE_WORKING: switch(m_stage) { - case MSAStage::Idle: { + case AuthStage::Initial: { QString loginMessage = tr("Logging in as %1 user"); if(m_data->type == AccountType::MSA) { return loginMessage.arg("Microsoft"); @@ -744,14 +744,16 @@ QString AuthContext::getStateMessage() const { return loginMessage.arg("Mojang"); } } - case MSAStage::UserAuth: + case AuthStage::UserAuth: return tr("Logging in as XBox user"); - case MSAStage::XboxAuth: + case AuthStage::XboxAuth: return tr("Logging in with XBox and Mojang services"); - case MSAStage::MinecraftProfile: + case AuthStage::MinecraftProfile: return tr("Getting Minecraft profile"); - case MSAStage::Skin: + case AuthStage::Skin: return tr("Getting Minecraft skin"); + case AuthStage::Complete: + return tr("Finished"); default: break; } diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index 5f99dba3..1d9f8f72 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -36,8 +36,7 @@ private slots: // OAuth-specific callbacks void onOAuthLinkingSucceeded(); void onOAuthLinkingFailed(); - void onOpenBrowser(const QUrl &url); - void onCloseBrowser(); + void onOAuthActivityChanged(Katabasis::Activity activity); // Yggdrasil specific callbacks @@ -82,13 +81,16 @@ protected: bool m_xboxProfileSucceeded = false; bool m_mcAuthSucceeded = false; Katabasis::Activity m_activity = Katabasis::Activity::Idle; - enum class MSAStage { - Idle, + enum class AuthStage { + Initial, UserAuth, XboxAuth, MinecraftProfile, - Skin - } m_stage = MSAStage::Idle; + Skin, + Complete + } m_stage = AuthStage::Initial; + + void setStage(AuthStage stage); QNetworkAccessManager *mgr = nullptr; }; diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h index 4361691c..9dbe5c71 100644 --- a/libraries/katabasis/include/katabasis/OAuth2.h +++ b/libraries/katabasis/include/katabasis/OAuth2.h @@ -140,7 +140,7 @@ signals: void closeBrowser(); /// Emitted when client needs to show a verification uri and user code - void showVerificationUriAndCode(const QUrl &uri, const QString &code); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); /// Emitted when authentication/deauthentication succeeded. void linkingSucceeded(); @@ -181,7 +181,7 @@ protected: void setExpires(QDateTime v); /// Start polling authorization server - void startPollServer(const QVariantMap ¶ms); + void startPollServer(const QVariantMap ¶ms, int expiresIn); /// Set authentication token. void setToken(const QString &v); diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp index 3c07420c..9756d377 100644 --- a/libraries/katabasis/src/OAuth2.cpp +++ b/libraries/katabasis/src/OAuth2.cpp @@ -472,16 +472,8 @@ void OAuth2::setExpires(QDateTime v) { token_.notAfter = v; } -void OAuth2::startPollServer(const QVariantMap ¶ms) +void OAuth2::startPollServer(const QVariantMap ¶ms, int expiresIn) { - bool ok = false; - int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); - if (!ok) { - qWarning() << "OAuth2::startPollServer: No expired_in parameter"; - emit linkingFailed(); - return; - } - qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; QUrl url(options_.accessTokenUrl); @@ -502,6 +494,7 @@ void OAuth2::startPollServer(const QVariantMap ¶ms) PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this); if (params.contains(OAUTH2_INTERVAL)) { + bool ok = false; int interval = params[OAUTH2_INTERVAL].toInt(&ok); if (ok) pollServer->setInterval(interval); @@ -629,9 +622,17 @@ void OAuth2::onDeviceAuthReplyFinished() if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); - emit showVerificationUriAndCode(uri, userCode); + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "OAuth2::startPollServer: No expired_in parameter"; + emit linkingFailed(); + return; + } - startPollServer(params); + emit showVerificationUriAndCode(uri, userCode, expiresIn); + + startPollServer(params, expiresIn); } else { qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; emit linkingFailed();