diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index d881a7691..b8a6db5ea 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -27,7 +27,7 @@ class AuthFlow : public Task { signals: void authorizeWithBrowser(const QUrl& url); - void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); + void authorizeWithBrowserWithExtra(const QUrl& verificationUrl, const QString& code, const QUrl& completeVerificationUrl); protected: void succeed(); diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index 3feb6852c..13c26c1a0 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -39,15 +39,55 @@ #include #include "Application.h" -#include "Json.h" -#include "net/RawHeaderProxy.h" -// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) { m_clientId = APPLICATION->getMSAClientID(); - connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort); - connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser); + m_oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")); + m_oauth2.setTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); + m_oauth2.setRequestedScopeTokens({ "XboxLive.SignIn", "XboxLive.offline_access" }); + m_oauth2.setClientIdentifier(m_clientId); + m_oauth2.setNetworkAccessManager(APPLICATION->network()); + + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::granted, this, [this] { + m_data->msaClientID = m_oauth2.clientIdentifier(); + m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); + m_data->msaToken.notAfter = m_oauth2.expirationAt(); + m_data->msaToken.extra = m_oauth2.extraTokens(); + m_data->msaToken.refresh_token = m_oauth2.refreshToken(); + m_data->msaToken.token = m_oauth2.token(); + emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + }); + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::authorizeWithUserCode, this, &MSADeviceCodeStep::authorizeWithBrowser); + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) { + auto state = AccountTaskState::STATE_FAILED_HARD; + if (m_oauth2.status() == QAbstractOAuth::Status::Granted) { + if (err == QAbstractOAuth2::Error::NetworkError) { + state = AccountTaskState::STATE_OFFLINE; + } else { + state = AccountTaskState::STATE_FAILED_SOFT; + } + } + auto message = tr("Microsoft user authentication failed."); + qWarning() << message; + emit finished(state, message); + }); + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::serverReportedErrorOccurred, this, + [this](const QString& error, const QString& errorDescription, const QUrl& uri) { + qWarning() << "Failed to login because" << error << errorDescription; + emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription); + }); + + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::extraTokensChanged, this, + [this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; }); + + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::clientIdentifierChanged, this, + [this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; }); + connect(&m_oauth2, &QOAuth2DeviceAuthorizationFlow::userCodeExpirationAtChanged, this, [](const QDateTime& expiration) { + qDebug() << "============="; + qDebug() << expiration; + qDebug() << "============="; + }); } QString MSADeviceCodeStep::describe() @@ -57,27 +97,9 @@ QString MSADeviceCodeStep::describe() void MSADeviceCodeStep::perform() { - QUrlQuery data; - data.addQueryItem("client_id", m_clientId); - data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access"); - auto payload = data.query(QUrl::FullyEncoded).toUtf8(); - QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"); - auto headers = QList{ - { "Content-Type", "application/x-www-form-urlencoded" }, - { "Accept", "application/json" }, - }; - auto [request, response] = Net::Upload::makeByteArray(url, payload); - m_request = request; - m_request->addHeaderProxy(std::make_unique(headers)); - m_request->enableAutoRetry(true); - - m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); - m_task->setAskRetry(false); - m_task->addNetAction(m_request); - - connect(m_task.get(), &Task::finished, this, [this, response] { deviceAuthorizationFinished(response); }); - - m_task->start(); + *m_data = AccountData(); + m_data->msaClientID = m_clientId; + m_oauth2.grant(); } struct DeviceAuthorizationResponse { @@ -91,188 +113,10 @@ struct DeviceAuthorizationResponse { QString error_description; }; -DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& data) -{ - QJsonParseError err; - QJsonDocument doc = QJsonDocument::fromJson(data, &err); - if (err.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); - return {}; - } - - if (!doc.isObject()) { - qWarning() << "Device authorization response is not an object"; - return {}; - } - auto obj = doc.object(); - return { - obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(), - obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(), - }; -} - -void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response) -{ - auto rsp = parseDeviceAuthorizationResponse(*response); - if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { - qWarning() << "Device authorization failed:" << rsp.error; - emit finished(AccountTaskState::STATE_FAILED_HARD, - tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); - return; - } - if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { - qWarning() << "Device authorization failed:" << *response; - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); - return; - } - - if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { - qWarning() << "Device authorization failed: required fields missing"; - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); - return; - } - if (rsp.interval != 0) { - interval = rsp.interval; - } - m_device_code = rsp.device_code; - emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in); - m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); - m_expiration_timer.setInterval(rsp.expires_in * 1000); - m_expiration_timer.setSingleShot(true); - m_expiration_timer.start(); - m_pool_timer.setTimerType(Qt::VeryCoarseTimer); - m_pool_timer.setSingleShot(true); - startPoolTimer(); -} - void MSADeviceCodeStep::abort() { - m_expiration_timer.stop(); - m_pool_timer.stop(); - if (m_request) { - m_request->abort(); - } - m_is_aborted = true; + if (m_oauth2.isPolling()) + m_oauth2.stopTokenPolling(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted")); } - -void MSADeviceCodeStep::startPoolTimer() -{ - if (m_is_aborted) { - return; - } - if (m_expiration_timer.remainingTime() < interval * 1000) { - perform(); - return; - } - - m_pool_timer.setInterval(interval * 1000); - m_pool_timer.start(); -} - -void MSADeviceCodeStep::authenticateUser() -{ - QUrlQuery data; - data.addQueryItem("client_id", m_clientId); - data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); - data.addQueryItem("device_code", m_device_code); - auto payload = data.query(QUrl::FullyEncoded).toUtf8(); - QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"); - auto headers = QList{ - { "Content-Type", "application/x-www-form-urlencoded" }, - { "Accept", "application/json" }, - }; - auto [request, response] = Net::Upload::makeByteArray(url, payload); - m_request = request; - m_request->addHeaderProxy(std::make_unique(headers)); - - connect(m_request.get(), &Task::finished, this, [this, response] { authenticationFinished(response); }); - - m_request->setNetwork(APPLICATION->network()); - m_request->start(); -} - -struct AuthenticationResponse { - QString access_token; - QString token_type; - QString refresh_token; - int expires_in; - - QString error; - QString error_description; - - QVariantMap extra; -}; - -AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) -{ - QJsonParseError err; - QJsonDocument doc = QJsonDocument::fromJson(data, &err); - if (err.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); - return {}; - } - - if (!doc.isObject()) { - qWarning() << "Device authorization response is not an object"; - return {}; - } - auto obj = doc.object(); - return { obj["access_token"].toString(), - obj["token_type"].toString(), - obj["refresh_token"].toString(), - obj["expires_in"].toInt(), - obj["error"].toString(), - obj["error_description"].toString(), - obj.toVariantMap() }; -} - -void MSADeviceCodeStep::authenticationFinished(QByteArray* response) -{ - if (m_request->error() == QNetworkReply::TimeoutError) { - // rfc8628#section-3.5 - // "On encountering a connection timeout, clients MUST unilaterally - // reduce their polling frequency before retrying. The use of an - // exponential backoff algorithm to achieve this, such as doubling the - // polling interval on each such connection timeout, is RECOMMENDED." - interval *= 2; - startPoolTimer(); - return; - } - auto rsp = parseAuthenticationResponse(*response); - if (rsp.error == "slow_down") { - // rfc8628#section-3.5 - // "A variant of 'authorization_pending', the authorization request is - // still pending and polling should continue, but the interval MUST - // be increased by 5 seconds for this and all subsequent requests." - interval += 5; - startPoolTimer(); - return; - } - if (rsp.error == "authorization_pending") { - // keep trying - rfc8628#section-3.5 - // "The authorization request is still pending as the end user hasn't - // yet completed the user-interaction steps (Section 3.3)." - startPoolTimer(); - return; - } - if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { - qWarning() << "Device Access failed:" << rsp.error; - emit finished(AccountTaskState::STATE_FAILED_HARD, - tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); - return; - } - if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { - startPoolTimer(); // it failed so just try again without increasing the interval - return; - } - - m_expiration_timer.stop(); - m_data->msaClientID = m_clientId; - m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); - m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in); - m_data->msaToken.extra = rsp.extra; - m_data->msaToken.refresh_token = rsp.refresh_token; - m_data->msaToken.token = rsp.access_token; - emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); -} diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index cfb8270d4..fe9cff189 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -35,11 +35,10 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" -#include "net/NetJob.h" -#include "net/Upload.h" + +#include class MSADeviceCodeStep : public AuthStep { Q_OBJECT @@ -55,23 +54,9 @@ class MSADeviceCodeStep : public AuthStep { void abort() override; signals: - void authorizeWithBrowser(QString url, QString code, int expiresIn); - - private slots: - void deviceAuthorizationFinished(QByteArray* response); - void startPoolTimer(); - void authenticateUser(); - void authenticationFinished(QByteArray* response); + void authorizeWithBrowser(const QUrl& verificationUrl, const QString& code, const QUrl& completeVerificationUrl); private: QString m_clientId; - QString m_device_code; - bool m_is_aborted = false; - int interval = 5; - - QTimer m_pool_timer; - QTimer m_expiration_timer; - - Net::Upload::Ptr m_request; - NetJob::Ptr m_task; + QOAuth2DeviceAuthorizationFlow m_oauth2; }; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 51a5e5ce0..93137f66f 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -133,8 +133,8 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile m_oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); } m_oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); - m_oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); - m_oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); + m_oauth2.setTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); + m_oauth2.setRequestedScopeTokens({ "XboxLive.SignIn", "XboxLive.offline_access" }); m_oauth2.setClientIdentifier(m_clientId); m_oauth2.setNetworkAccessManager(APPLICATION->network()); @@ -164,7 +164,7 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile qWarning() << message; emit finished(state, message); }); - connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::error, this, + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::serverReportedErrorOccurred, this, [this](const QString& error, const QString& errorDescription, const QUrl& uri) { qWarning() << "Failed to login because" << error << errorDescription; emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription); @@ -195,7 +195,7 @@ void MSAStep::perform() return; } m_oauth2.setRefreshToken(m_data->msaToken.refresh_token); - m_oauth2.refreshAccessToken(); + m_oauth2.refreshTokens(); } else { m_oauth2.setModifyParametersFunction( [](QAbstractOAuth::Stage stage, QMultiMap* map) { map->insert("prompt", "select_account"); }); diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h index 2f4e7812b..baab1895a 100644 --- a/launcher/minecraft/auth/steps/MSAStep.h +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -38,7 +38,8 @@ #include "minecraft/auth/AuthStep.h" -#include +#include + class MSAStep : public AuthStep { Q_OBJECT public: diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index e238a54eb..c269f0126 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -179,15 +179,20 @@ void paintQR(QPainter& painter, const QSize canvasSize, const QString& data, QCo } } -void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) +void MSALoginDialog::authorizeWithBrowserWithExtra(const QUrl& verificationUrl, const QString& code, const QUrl& completeVerificationUrl) { ui->stackedWidget->setCurrentIndex(1); ui->stackedWidget->adjustSize(); ui->stackedWidget->updateGeometry(); this->adjustSize(); + auto url = verificationUrl.toString(); + if (completeVerificationUrl.isValid()) { + url = completeVerificationUrl.toString(); + } + const auto linkString = QString("%2").arg(url, url); - if (url == "https://www.microsoft.com/link" && !code.isEmpty()) { + if (!completeVerificationUrl.isValid() && url == "https://www.microsoft.com/link" && !code.isEmpty()) { url += QString("?otc=%1").arg(code); } ui->code->setText(code); diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index f19abbe6d..86a1b94ee 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -42,7 +42,7 @@ class MSALoginDialog : public QDialog { void onDeviceFlowStatus(QString status); void onAuthFlowStatus(QString status); void authorizeWithBrowser(const QUrl& url); - void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); + void authorizeWithBrowserWithExtra(const QUrl& verificationUrl, const QString& code, const QUrl& completeVerificationUrl); private: Ui::MSALoginDialog* ui;