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.
This commit is contained in:
parent
50b92c1af2
commit
eae65da110
@ -41,6 +41,9 @@ int MSALoginDialog::exec() {
|
|||||||
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
|
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
|
||||||
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
|
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
|
||||||
connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
|
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();
|
m_loginTask->start();
|
||||||
|
|
||||||
return QDialog::exec();
|
return QDialog::exec();
|
||||||
@ -52,6 +55,37 @@ MSALoginDialog::~MSALoginDialog()
|
|||||||
delete ui;
|
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("<a href=\"%1\">%2</a>").arg(urlString, urlString);
|
||||||
|
ui->label->setText(tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::hideVerificationUriAndCode() {
|
||||||
|
m_externalLoginTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
void MSALoginDialog::setUserInputsEnabled(bool enable)
|
void MSALoginDialog::setUserInputsEnabled(bool enable)
|
||||||
{
|
{
|
||||||
ui->buttonBox->setEnabled(enable);
|
ui->buttonBox->setEnabled(enable);
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include <QtWidgets/QDialog>
|
#include <QtWidgets/QDialog>
|
||||||
#include <QtCore/QEventLoop>
|
#include <QtCore/QEventLoop>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "minecraft/auth/MinecraftAccount.h"
|
#include "minecraft/auth/MinecraftAccount.h"
|
||||||
|
|
||||||
@ -46,10 +47,17 @@ slots:
|
|||||||
void onTaskSucceeded();
|
void onTaskSucceeded();
|
||||||
void onTaskStatus(const QString &status);
|
void onTaskStatus(const QString &status);
|
||||||
void onTaskProgress(qint64 current, qint64 total);
|
void onTaskProgress(qint64 current, qint64 total);
|
||||||
|
void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
|
||||||
|
void hideVerificationUriAndCode();
|
||||||
|
|
||||||
|
void externalLoginTick();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::MSALoginDialog *ui;
|
Ui::MSALoginDialog *ui;
|
||||||
MinecraftAccountPtr m_account;
|
MinecraftAccountPtr m_account;
|
||||||
std::shared_ptr<Task> m_loginTask;
|
std::shared_ptr<AccountTask> m_loginTask;
|
||||||
|
QTimer m_externalLoginTimer;
|
||||||
|
int m_externalLoginElapsed = 0;
|
||||||
|
int m_externalLoginTimeout = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,6 +30,9 @@ aaaaa</string>
|
|||||||
<property name="textFormat">
|
<property name="textFormat">
|
||||||
<enum>Qt::RichText</enum>
|
<enum>Qt::RichText</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="textInteractionFlags">
|
<property name="textInteractionFlags">
|
||||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||||
</property>
|
</property>
|
||||||
|
@ -83,6 +83,10 @@ public:
|
|||||||
return m_accountState;
|
return m_accountState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
|
||||||
|
void hideVerificationUriAndCode();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,7 +43,7 @@ void AuthContext::finishActivity() {
|
|||||||
throw 0;
|
throw 0;
|
||||||
}
|
}
|
||||||
m_activity = Katabasis::Activity::Idle;
|
m_activity = Katabasis::Activity::Idle;
|
||||||
m_stage = MSAStage::Idle;
|
setStage(AuthStage::Complete);
|
||||||
m_data->validity_ = m_data->minecraftProfile.validity;
|
m_data->validity_ = m_data->minecraftProfile.validity;
|
||||||
emit activityChanged(m_activity);
|
emit activityChanged(m_activity);
|
||||||
}
|
}
|
||||||
@ -55,16 +55,16 @@ void AuthContext::initMSA() {
|
|||||||
Katabasis::OAuth2::Options opts;
|
Katabasis::OAuth2::Options opts;
|
||||||
opts.scope = "XboxLive.signin offline_access";
|
opts.scope = "XboxLive.signin offline_access";
|
||||||
opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID;
|
opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID;
|
||||||
opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
|
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
|
||||||
opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
|
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
||||||
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
|
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
|
||||||
|
|
||||||
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr);
|
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::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
|
||||||
connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
|
connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
|
||||||
connect(m_oauth2, &OAuth2::openBrowser, this, &AuthContext::onOpenBrowser);
|
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
|
||||||
connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser);
|
|
||||||
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
|
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() {
|
void AuthContext::onOAuthLinkingFailed() {
|
||||||
|
emit hideVerificationUriAndCode();
|
||||||
finishActivity();
|
finishActivity();
|
||||||
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
|
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AuthContext::onOAuthLinkingSucceeded() {
|
void AuthContext::onOAuthLinkingSucceeded() {
|
||||||
|
emit hideVerificationUriAndCode();
|
||||||
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
||||||
if (!o2t->linked()) {
|
if (!o2t->linked()) {
|
||||||
finishActivity();
|
finishActivity();
|
||||||
@ -143,7 +137,7 @@ void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AuthContext::doUserAuth() {
|
void AuthContext::doUserAuth() {
|
||||||
m_stage = MSAStage::UserAuth;
|
setStage(AuthStage::UserAuth);
|
||||||
changeState(STATE_WORKING, tr("Starting user authentication"));
|
changeState(STATE_WORKING, tr("Starting user authentication"));
|
||||||
|
|
||||||
QString xbox_auth_template = R"XXX(
|
QString xbox_auth_template = R"XXX(
|
||||||
@ -307,7 +301,7 @@ void AuthContext::onUserAuthDone(
|
|||||||
}
|
}
|
||||||
m_data->userToken = temp;
|
m_data->userToken = temp;
|
||||||
|
|
||||||
m_stage = MSAStage::XboxAuth;
|
setStage(AuthStage::XboxAuth);
|
||||||
changeState(STATE_WORKING, tr("Starting XBox authentication"));
|
changeState(STATE_WORKING, tr("Starting XBox authentication"));
|
||||||
|
|
||||||
doSTSAuthMinecraft();
|
doSTSAuthMinecraft();
|
||||||
@ -671,7 +665,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AuthContext::doMinecraftProfile() {
|
void AuthContext::doMinecraftProfile() {
|
||||||
m_stage = MSAStage::MinecraftProfile;
|
setStage(AuthStage::MinecraftProfile);
|
||||||
changeState(STATE_WORKING, tr("Starting minecraft profile acquisition"));
|
changeState(STATE_WORKING, tr("Starting minecraft profile acquisition"));
|
||||||
|
|
||||||
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
|
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
|
||||||
@ -710,7 +704,7 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AuthContext::doGetSkin() {
|
void AuthContext::doGetSkin() {
|
||||||
m_stage = MSAStage::Skin;
|
setStage(AuthStage::Skin);
|
||||||
changeState(STATE_WORKING, tr("Fetching player skin"));
|
changeState(STATE_WORKING, tr("Fetching player skin"));
|
||||||
|
|
||||||
auto url = QUrl(m_data->minecraftProfile.skin.url);
|
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"));
|
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 {
|
QString AuthContext::getStateMessage() const {
|
||||||
switch (m_accountState)
|
switch (m_accountState)
|
||||||
{
|
{
|
||||||
case STATE_WORKING:
|
case STATE_WORKING:
|
||||||
switch(m_stage) {
|
switch(m_stage) {
|
||||||
case MSAStage::Idle: {
|
case AuthStage::Initial: {
|
||||||
QString loginMessage = tr("Logging in as %1 user");
|
QString loginMessage = tr("Logging in as %1 user");
|
||||||
if(m_data->type == AccountType::MSA) {
|
if(m_data->type == AccountType::MSA) {
|
||||||
return loginMessage.arg("Microsoft");
|
return loginMessage.arg("Microsoft");
|
||||||
@ -744,14 +744,16 @@ QString AuthContext::getStateMessage() const {
|
|||||||
return loginMessage.arg("Mojang");
|
return loginMessage.arg("Mojang");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case MSAStage::UserAuth:
|
case AuthStage::UserAuth:
|
||||||
return tr("Logging in as XBox user");
|
return tr("Logging in as XBox user");
|
||||||
case MSAStage::XboxAuth:
|
case AuthStage::XboxAuth:
|
||||||
return tr("Logging in with XBox and Mojang services");
|
return tr("Logging in with XBox and Mojang services");
|
||||||
case MSAStage::MinecraftProfile:
|
case AuthStage::MinecraftProfile:
|
||||||
return tr("Getting Minecraft profile");
|
return tr("Getting Minecraft profile");
|
||||||
case MSAStage::Skin:
|
case AuthStage::Skin:
|
||||||
return tr("Getting Minecraft skin");
|
return tr("Getting Minecraft skin");
|
||||||
|
case AuthStage::Complete:
|
||||||
|
return tr("Finished");
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,7 @@ private slots:
|
|||||||
// OAuth-specific callbacks
|
// OAuth-specific callbacks
|
||||||
void onOAuthLinkingSucceeded();
|
void onOAuthLinkingSucceeded();
|
||||||
void onOAuthLinkingFailed();
|
void onOAuthLinkingFailed();
|
||||||
void onOpenBrowser(const QUrl &url);
|
|
||||||
void onCloseBrowser();
|
|
||||||
void onOAuthActivityChanged(Katabasis::Activity activity);
|
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||||
|
|
||||||
// Yggdrasil specific callbacks
|
// Yggdrasil specific callbacks
|
||||||
@ -82,13 +81,16 @@ protected:
|
|||||||
bool m_xboxProfileSucceeded = false;
|
bool m_xboxProfileSucceeded = false;
|
||||||
bool m_mcAuthSucceeded = false;
|
bool m_mcAuthSucceeded = false;
|
||||||
Katabasis::Activity m_activity = Katabasis::Activity::Idle;
|
Katabasis::Activity m_activity = Katabasis::Activity::Idle;
|
||||||
enum class MSAStage {
|
enum class AuthStage {
|
||||||
Idle,
|
Initial,
|
||||||
UserAuth,
|
UserAuth,
|
||||||
XboxAuth,
|
XboxAuth,
|
||||||
MinecraftProfile,
|
MinecraftProfile,
|
||||||
Skin
|
Skin,
|
||||||
} m_stage = MSAStage::Idle;
|
Complete
|
||||||
|
} m_stage = AuthStage::Initial;
|
||||||
|
|
||||||
|
void setStage(AuthStage stage);
|
||||||
|
|
||||||
QNetworkAccessManager *mgr = nullptr;
|
QNetworkAccessManager *mgr = nullptr;
|
||||||
};
|
};
|
||||||
|
@ -140,7 +140,7 @@ signals:
|
|||||||
void closeBrowser();
|
void closeBrowser();
|
||||||
|
|
||||||
/// Emitted when client needs to show a verification uri and user code
|
/// 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.
|
/// Emitted when authentication/deauthentication succeeded.
|
||||||
void linkingSucceeded();
|
void linkingSucceeded();
|
||||||
@ -181,7 +181,7 @@ protected:
|
|||||||
void setExpires(QDateTime v);
|
void setExpires(QDateTime v);
|
||||||
|
|
||||||
/// Start polling authorization server
|
/// Start polling authorization server
|
||||||
void startPollServer(const QVariantMap ¶ms);
|
void startPollServer(const QVariantMap ¶ms, int expiresIn);
|
||||||
|
|
||||||
/// Set authentication token.
|
/// Set authentication token.
|
||||||
void setToken(const QString &v);
|
void setToken(const QString &v);
|
||||||
|
@ -472,16 +472,8 @@ void OAuth2::setExpires(QDateTime v) {
|
|||||||
token_.notAfter = 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";
|
qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds";
|
||||||
|
|
||||||
QUrl url(options_.accessTokenUrl);
|
QUrl url(options_.accessTokenUrl);
|
||||||
@ -502,6 +494,7 @@ void OAuth2::startPollServer(const QVariantMap ¶ms)
|
|||||||
|
|
||||||
PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this);
|
PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this);
|
||||||
if (params.contains(OAUTH2_INTERVAL)) {
|
if (params.contains(OAUTH2_INTERVAL)) {
|
||||||
|
bool ok = false;
|
||||||
int interval = params[OAUTH2_INTERVAL].toInt(&ok);
|
int interval = params[OAUTH2_INTERVAL].toInt(&ok);
|
||||||
if (ok)
|
if (ok)
|
||||||
pollServer->setInterval(interval);
|
pollServer->setInterval(interval);
|
||||||
@ -629,9 +622,17 @@ void OAuth2::onDeviceAuthReplyFinished()
|
|||||||
if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
|
if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
|
||||||
emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl());
|
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 {
|
} else {
|
||||||
qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response";
|
qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response";
|
||||||
emit linkingFailed();
|
emit linkingFailed();
|
||||||
|
Loading…
Reference in New Issue
Block a user