mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-04-30 06:34:27 +02:00
Add back device code flow
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
parent
2a58fb0ac5
commit
09c0c11033
@ -228,6 +228,8 @@ set(MINECRAFT_SOURCES
|
|||||||
minecraft/auth/steps/LauncherLoginStep.h
|
minecraft/auth/steps/LauncherLoginStep.h
|
||||||
minecraft/auth/steps/MinecraftProfileStep.cpp
|
minecraft/auth/steps/MinecraftProfileStep.cpp
|
||||||
minecraft/auth/steps/MinecraftProfileStep.h
|
minecraft/auth/steps/MinecraftProfileStep.h
|
||||||
|
minecraft/auth/steps/MSADeviceCodeStep.cpp
|
||||||
|
minecraft/auth/steps/MSADeviceCodeStep.h
|
||||||
minecraft/auth/steps/MSAStep.cpp
|
minecraft/auth/steps/MSAStep.cpp
|
||||||
minecraft/auth/steps/MSAStep.h
|
minecraft/auth/steps/MSAStep.h
|
||||||
minecraft/auth/steps/XboxAuthorizationStep.cpp
|
minecraft/auth/steps/XboxAuthorizationStep.cpp
|
||||||
|
@ -7,22 +7,31 @@
|
|||||||
#include "minecraft/auth/steps/EntitlementsStep.h"
|
#include "minecraft/auth/steps/EntitlementsStep.h"
|
||||||
#include "minecraft/auth/steps/GetSkinStep.h"
|
#include "minecraft/auth/steps/GetSkinStep.h"
|
||||||
#include "minecraft/auth/steps/LauncherLoginStep.h"
|
#include "minecraft/auth/steps/LauncherLoginStep.h"
|
||||||
|
#include "minecraft/auth/steps/MSADeviceCodeStep.h"
|
||||||
#include "minecraft/auth/steps/MSAStep.h"
|
#include "minecraft/auth/steps/MSAStep.h"
|
||||||
#include "minecraft/auth/steps/MinecraftProfileStep.h"
|
#include "minecraft/auth/steps/MinecraftProfileStep.h"
|
||||||
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
|
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
|
||||||
#include "minecraft/auth/steps/XboxProfileStep.h"
|
#include "minecraft/auth/steps/XboxProfileStep.h"
|
||||||
#include "minecraft/auth/steps/XboxUserStep.h"
|
#include "minecraft/auth/steps/XboxUserStep.h"
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
#include "AuthFlow.h"
|
#include "AuthFlow.h"
|
||||||
|
|
||||||
#include <Application.h>
|
#include <Application.h>
|
||||||
|
|
||||||
AuthFlow::AuthFlow(AccountData* data, bool silent, QObject* parent) : Task(parent), m_data(data)
|
AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data)
|
||||||
{
|
{
|
||||||
if (data->type == AccountType::MSA) {
|
if (data->type == AccountType::MSA) {
|
||||||
auto oauthStep = makeShared<MSAStep>(m_data, silent);
|
if (action == Action::DeviceCode) {
|
||||||
|
auto oauthStep = makeShared<MSADeviceCodeStep>(m_data);
|
||||||
|
connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra);
|
||||||
|
connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort);
|
||||||
|
m_steps.append(oauthStep);
|
||||||
|
} else {
|
||||||
|
auto oauthStep = makeShared<MSAStep>(m_data, action == Action::Refresh);
|
||||||
connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser);
|
connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser);
|
||||||
m_steps.append(oauthStep);
|
m_steps.append(oauthStep);
|
||||||
|
}
|
||||||
m_steps.append(makeShared<XboxUserStep>(m_data));
|
m_steps.append(makeShared<XboxUserStep>(m_data));
|
||||||
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
|
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
|
||||||
m_steps.append(
|
m_steps.append(
|
||||||
|
@ -15,7 +15,9 @@ class AuthFlow : public Task {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AuthFlow(AccountData* data, bool silent = false, QObject* parent = 0);
|
enum class Action { Refresh, Login, DeviceCode };
|
||||||
|
|
||||||
|
explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0);
|
||||||
virtual ~AuthFlow() = default;
|
virtual ~AuthFlow() = default;
|
||||||
|
|
||||||
void executeTask() override;
|
void executeTask() override;
|
||||||
@ -24,6 +26,7 @@ class AuthFlow : public Task {
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void authorizeWithBrowser(const QUrl& url);
|
void authorizeWithBrowser(const QUrl& url);
|
||||||
|
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void succeed();
|
void succeed();
|
||||||
|
@ -119,11 +119,11 @@ QPixmap MinecraftAccount::getFace() const
|
|||||||
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_qobject_ptr<AuthFlow> MinecraftAccount::login()
|
shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode)
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_currentTask.get() == nullptr);
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||||
|
|
||||||
m_currentTask.reset(new AuthFlow(&data, false, this));
|
m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this));
|
||||||
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
||||||
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
||||||
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
|
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
|
||||||
@ -137,7 +137,7 @@ shared_qobject_ptr<AuthFlow> MinecraftAccount::refresh()
|
|||||||
return m_currentTask;
|
return m_currentTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentTask.reset(new AuthFlow(&data, true, this));
|
m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this));
|
||||||
|
|
||||||
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
||||||
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
||||||
|
@ -95,7 +95,7 @@ class MinecraftAccount : public QObject, public Usable {
|
|||||||
QJsonObject saveToJson() const;
|
QJsonObject saveToJson() const;
|
||||||
|
|
||||||
public: /* manipulation */
|
public: /* manipulation */
|
||||||
shared_qobject_ptr<AuthFlow> login();
|
shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false);
|
||||||
|
|
||||||
shared_qobject_ptr<AuthFlow> refresh();
|
shared_qobject_ptr<AuthFlow> refresh();
|
||||||
|
|
||||||
|
272
launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp
Normal file
272
launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Prism Launcher - Minecraft Launcher
|
||||||
|
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This file incorporates work covered by the following copyright and
|
||||||
|
* permission notice:
|
||||||
|
*
|
||||||
|
* 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 "MSADeviceCodeStep.h"
|
||||||
|
#include <qdatetime.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "Application.h"
|
||||||
|
#include "Json.h"
|
||||||
|
#include "net/StaticHeaderProxy.h"
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code
|
||||||
|
MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data)
|
||||||
|
{
|
||||||
|
m_clientId = APPLICATION->getMSAClientID();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MSADeviceCodeStep::describe()
|
||||||
|
{
|
||||||
|
return tr("Logging in with Microsoft account(device code).");
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Net::HeaderPair>{
|
||||||
|
{ "Content-Type", "application/x-www-form-urlencoded" },
|
||||||
|
{ "Accept", "application/json" },
|
||||||
|
};
|
||||||
|
m_response.reset(new QByteArray());
|
||||||
|
m_task = Net::Upload::makeByteArray(url, m_response, payload);
|
||||||
|
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||||
|
|
||||||
|
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished);
|
||||||
|
|
||||||
|
m_task->setNetwork(APPLICATION->network());
|
||||||
|
m_task->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeviceAutorizationResponse {
|
||||||
|
QString device_code;
|
||||||
|
QString user_code;
|
||||||
|
QString verification_uri;
|
||||||
|
int expires_in;
|
||||||
|
int interval;
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
QString error_description;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& data)
|
||||||
|
{
|
||||||
|
QJsonParseError err;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
|
||||||
|
if (err.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
qWarning() << "Device autorization response is not an object";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto obj = doc.object();
|
||||||
|
return {
|
||||||
|
Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"),
|
||||||
|
Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"),
|
||||||
|
Json::ensureString(obj, "error_description"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSADeviceCodeStep::deviceAutorizationFinished()
|
||||||
|
{
|
||||||
|
auto rsp = parseDeviceAutorizationResponse(*m_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_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
|
||||||
|
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization"));
|
||||||
|
qDebug() << *m_response;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) {
|
||||||
|
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);
|
||||||
|
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
|
||||||
|
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_task) {
|
||||||
|
m_task->abort();
|
||||||
|
}
|
||||||
|
m_is_aborted = true;
|
||||||
|
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSADeviceCodeStep::startPoolTimer()
|
||||||
|
{
|
||||||
|
if (m_is_aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_pool_timer.setInterval(interval * 1000);
|
||||||
|
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
|
||||||
|
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<Net::HeaderPair>{
|
||||||
|
{ "Content-Type", "application/x-www-form-urlencoded" },
|
||||||
|
{ "Accept", "application/json" },
|
||||||
|
};
|
||||||
|
m_response.reset(new QByteArray());
|
||||||
|
m_task = Net::Upload::makeByteArray(url, m_response, payload);
|
||||||
|
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||||
|
|
||||||
|
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished);
|
||||||
|
|
||||||
|
m_task->setNetwork(APPLICATION->network());
|
||||||
|
m_task->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 autorization response due to err:" << err.errorString();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
qWarning() << "Device autorization response is not an object";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto obj = doc.object();
|
||||||
|
return { Json::ensureString(obj, "access_token"),
|
||||||
|
Json::ensureString(obj, "token_type"),
|
||||||
|
Json::ensureString(obj, "refresh_token"),
|
||||||
|
Json::ensureInteger(obj, "expires_in"),
|
||||||
|
Json::ensureString(obj, "error"),
|
||||||
|
Json::ensureString(obj, "error_description"),
|
||||||
|
obj.toVariantMap() };
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSADeviceCodeStep::authenticationFinished()
|
||||||
|
{
|
||||||
|
if (m_task->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(*m_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_task->wasSuccessful() || m_task->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"));
|
||||||
|
}
|
76
launcher/minecraft/auth/steps/MSADeviceCodeStep.h
Normal file
76
launcher/minecraft/auth/steps/MSADeviceCodeStep.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Prism Launcher - Minecraft Launcher
|
||||||
|
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This file incorporates work covered by the following copyright and
|
||||||
|
* permission notice:
|
||||||
|
*
|
||||||
|
* 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 <QObject>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "minecraft/auth/AuthStep.h"
|
||||||
|
#include "net/Upload.h"
|
||||||
|
|
||||||
|
class MSADeviceCodeStep : public AuthStep {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MSADeviceCodeStep(AccountData* data);
|
||||||
|
virtual ~MSADeviceCodeStep() noexcept = default;
|
||||||
|
|
||||||
|
void perform() override;
|
||||||
|
|
||||||
|
QString describe() override;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void abort();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void authorizeWithBrowser(QString url, QString code, int expiresIn);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void deviceAutorizationFinished();
|
||||||
|
void startPoolTimer();
|
||||||
|
void authenticateUser();
|
||||||
|
void authenticationFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_clientId;
|
||||||
|
QString m_device_code;
|
||||||
|
bool m_is_aborted = false;
|
||||||
|
int interval = 5;
|
||||||
|
|
||||||
|
QTimer m_pool_timer;
|
||||||
|
QTimer m_expiration_timer;
|
||||||
|
|
||||||
|
std::shared_ptr<QByteArray> m_response;
|
||||||
|
Net::Upload::Ptr m_task;
|
||||||
|
};
|
@ -74,10 +74,6 @@ class ByteArraySink : public Sink {
|
|||||||
|
|
||||||
auto abort() -> Task::State override
|
auto abort() -> Task::State override
|
||||||
{
|
{
|
||||||
if (m_output)
|
|
||||||
m_output->clear();
|
|
||||||
else
|
|
||||||
qWarning() << "ByteArraySink did not clear the buffer because it's not addressable";
|
|
||||||
failAllValidators();
|
failAllValidators();
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
}
|
}
|
||||||
|
@ -256,21 +256,18 @@ void NetRequest::downloadFinished()
|
|||||||
{
|
{
|
||||||
qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString();
|
qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString();
|
||||||
m_sink->abort();
|
m_sink->abort();
|
||||||
m_reply.reset();
|
|
||||||
emit succeeded();
|
emit succeeded();
|
||||||
emit finished();
|
emit finished();
|
||||||
return;
|
return;
|
||||||
} else if (m_state == State::Failed) {
|
} else if (m_state == State::Failed) {
|
||||||
qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString();
|
qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString();
|
||||||
m_sink->abort();
|
m_sink->abort();
|
||||||
m_reply.reset();
|
emit failed(m_reply->errorString());
|
||||||
emit failed("");
|
|
||||||
emit finished();
|
emit finished();
|
||||||
return;
|
return;
|
||||||
} else if (m_state == State::AbortedByUser) {
|
} else if (m_state == State::AbortedByUser) {
|
||||||
qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString();
|
qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString();
|
||||||
m_sink->abort();
|
m_sink->abort();
|
||||||
m_reply.reset();
|
|
||||||
emit aborted();
|
emit aborted();
|
||||||
emit finished();
|
emit finished();
|
||||||
return;
|
return;
|
||||||
@ -284,7 +281,7 @@ void NetRequest::downloadFinished()
|
|||||||
if (m_state != State::Succeeded) {
|
if (m_state != State::Succeeded) {
|
||||||
qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString();
|
qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString();
|
||||||
m_sink->abort();
|
m_sink->abort();
|
||||||
emit failed("");
|
emit failed("failed to write in sink");
|
||||||
emit finished();
|
emit finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -295,13 +292,11 @@ void NetRequest::downloadFinished()
|
|||||||
if (m_state != State::Succeeded) {
|
if (m_state != State::Succeeded) {
|
||||||
qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString();
|
qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString();
|
||||||
m_sink->abort();
|
m_sink->abort();
|
||||||
m_reply.reset();
|
emit failed("failed to finalize the request");
|
||||||
emit failed("");
|
|
||||||
emit finished();
|
emit finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_reply.reset();
|
|
||||||
qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString();
|
qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString();
|
||||||
emit succeeded();
|
emit succeeded();
|
||||||
emit finished();
|
emit finished();
|
||||||
|
@ -46,6 +46,7 @@ namespace Net {
|
|||||||
|
|
||||||
QNetworkReply* Upload::getReply(QNetworkRequest& request)
|
QNetworkReply* Upload::getReply(QNetworkRequest& request)
|
||||||
{
|
{
|
||||||
|
if (!request.hasRawHeader("Content-Type"))
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
return m_network->post(request, m_post_data);
|
return m_network->post(request, m_post_data);
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS
|
|||||||
ui->cancel->setEnabled(false);
|
ui->cancel->setEnabled(false);
|
||||||
ui->link->setVisible(false);
|
ui->link->setVisible(false);
|
||||||
ui->copy->setVisible(false);
|
ui->copy->setVisible(false);
|
||||||
|
ui->progressBar->setVisible(false);
|
||||||
|
|
||||||
connect(ui->cancel, &QPushButton::pressed, this, &QDialog::reject);
|
connect(ui->cancel, &QPushButton::pressed, this, &QDialog::reject);
|
||||||
connect(ui->copy, &QPushButton::pressed, this, &MSALoginDialog::copyUrl);
|
connect(ui->copy, &QPushButton::pressed, this, &MSALoginDialog::copyUrl);
|
||||||
@ -60,12 +61,15 @@ int MSALoginDialog::exec()
|
|||||||
{
|
{
|
||||||
// Setup the login task and start it
|
// Setup the login task and start it
|
||||||
m_account = MinecraftAccount::createBlankMSA();
|
m_account = MinecraftAccount::createBlankMSA();
|
||||||
m_loginTask = m_account->login();
|
m_task = m_account->login(m_using_device_code);
|
||||||
connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
|
connect(m_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
|
||||||
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
|
connect(m_task.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
|
||||||
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
|
connect(m_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
|
||||||
connect(m_loginTask.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
|
connect(m_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
|
||||||
m_loginTask->start();
|
connect(m_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
|
||||||
|
connect(ui->cancel, &QPushButton::pressed, m_task.get(), &Task::abort);
|
||||||
|
connect(&m_external_timer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick);
|
||||||
|
m_task->start();
|
||||||
|
|
||||||
return QDialog::exec();
|
return QDialog::exec();
|
||||||
}
|
}
|
||||||
@ -101,12 +105,14 @@ void MSALoginDialog::onTaskStatus(const QString& status)
|
|||||||
ui->cancel->setEnabled(false);
|
ui->cancel->setEnabled(false);
|
||||||
ui->link->setVisible(false);
|
ui->link->setVisible(false);
|
||||||
ui->copy->setVisible(false);
|
ui->copy->setVisible(false);
|
||||||
|
ui->progressBar->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public interface
|
// Public interface
|
||||||
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg)
|
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg, bool usingDeviceCode)
|
||||||
{
|
{
|
||||||
MSALoginDialog dlg(parent);
|
MSALoginDialog dlg(parent);
|
||||||
|
dlg.m_using_device_code = usingDeviceCode;
|
||||||
dlg.ui->message->setText(msg);
|
dlg.ui->message->setText(msg);
|
||||||
if (dlg.exec() == QDialog::Accepted) {
|
if (dlg.exec() == QDialog::Accepted) {
|
||||||
return dlg.m_account;
|
return dlg.m_account;
|
||||||
@ -132,3 +138,44 @@ void MSALoginDialog::copyUrl()
|
|||||||
QClipboard* cb = QApplication::clipboard();
|
QClipboard* cb = QApplication::clipboard();
|
||||||
cb->setText(ui->link->text());
|
cb->setText(ui->link->text());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn)
|
||||||
|
{
|
||||||
|
m_external_elapsed = 0;
|
||||||
|
m_external_timeout = expiresIn;
|
||||||
|
|
||||||
|
m_external_timer.setInterval(1000);
|
||||||
|
m_external_timer.setSingleShot(false);
|
||||||
|
m_external_timer.start();
|
||||||
|
|
||||||
|
ui->progressBar->setMaximum(expiresIn);
|
||||||
|
ui->progressBar->setValue(m_external_elapsed);
|
||||||
|
|
||||||
|
QString linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
|
||||||
|
if (url == "https://www.microsoft.com/link" && !code.isEmpty()) {
|
||||||
|
url += QString("?otc=%1").arg(code);
|
||||||
|
ui->message->setText(tr("<p>Please login in the opened browser. If no browser was opened, please open up %1 in "
|
||||||
|
"a browser and put in the code <b>%2</b> to proceed with login.</p>")
|
||||||
|
.arg(linkString, code));
|
||||||
|
} else {
|
||||||
|
ui->message->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));
|
||||||
|
}
|
||||||
|
ui->cancel->setEnabled(true);
|
||||||
|
ui->link->setVisible(true);
|
||||||
|
ui->copy->setVisible(true);
|
||||||
|
ui->progressBar->setVisible(true);
|
||||||
|
DesktopServices::openUrl(url);
|
||||||
|
ui->link->setText(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::externalLoginTick()
|
||||||
|
{
|
||||||
|
m_external_elapsed++;
|
||||||
|
ui->progressBar->setValue(m_external_elapsed);
|
||||||
|
ui->progressBar->repaint();
|
||||||
|
|
||||||
|
if (m_external_elapsed >= m_external_timeout) {
|
||||||
|
m_external_timer.stop();
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
#include <QtCore/QEventLoop>
|
#include <QtCore/QEventLoop>
|
||||||
#include <QtWidgets/QDialog>
|
#include <QtWidgets/QDialog>
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ class MSALoginDialog : public QDialog {
|
|||||||
public:
|
public:
|
||||||
~MSALoginDialog();
|
~MSALoginDialog();
|
||||||
|
|
||||||
static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
|
static MinecraftAccountPtr newAccount(QWidget* parent, QString message, bool usingDeviceCode = false);
|
||||||
int exec() override;
|
int exec() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -42,10 +43,18 @@ class MSALoginDialog : public QDialog {
|
|||||||
void onTaskSucceeded();
|
void onTaskSucceeded();
|
||||||
void onTaskStatus(const QString& status);
|
void onTaskStatus(const QString& status);
|
||||||
void authorizeWithBrowser(const QUrl& url);
|
void authorizeWithBrowser(const QUrl& url);
|
||||||
|
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
|
||||||
void copyUrl();
|
void copyUrl();
|
||||||
|
void externalLoginTick();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::MSALoginDialog* ui;
|
Ui::MSALoginDialog* ui;
|
||||||
MinecraftAccountPtr m_account;
|
MinecraftAccountPtr m_account;
|
||||||
shared_qobject_ptr<AuthFlow> m_loginTask;
|
shared_qobject_ptr<AuthFlow> m_task;
|
||||||
|
|
||||||
|
int m_external_elapsed;
|
||||||
|
int m_external_timeout;
|
||||||
|
QTimer m_external_timer;
|
||||||
|
|
||||||
|
bool m_using_device_code = false;
|
||||||
};
|
};
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>491</width>
|
<width>491</width>
|
||||||
<height>143</height>
|
<height>208</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
<sizepolicy hsizetype="Fixed" vsizetype="MinimumExpanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -22,12 +22,27 @@
|
|||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="message">
|
<widget class="QLabel" name="message">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>500</width>
|
||||||
|
<height>500</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
</property>
|
</property>
|
||||||
<property name="textFormat">
|
<property name="textFormat">
|
||||||
<enum>Qt::RichText</enum>
|
<enum>Qt::RichText</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="openExternalLinks">
|
<property name="openExternalLinks">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -51,12 +66,23 @@
|
|||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset theme="copy"/>
|
<iconset theme="copy">
|
||||||
|
<normaloff>.</normaloff>.</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QProgressBar" name="progressBar">
|
||||||
|
<property name="value">
|
||||||
|
<number>24</number>
|
||||||
|
</property>
|
||||||
|
<property name="textVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="cancel">
|
<widget class="QPushButton" name="cancel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
|
|
||||||
#include <QItemSelectionModel>
|
#include <QItemSelectionModel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
@ -134,8 +135,16 @@ void AccountListPage::listChanged()
|
|||||||
|
|
||||||
void AccountListPage::on_actionAddMicrosoft_triggered()
|
void AccountListPage::on_actionAddMicrosoft_triggered()
|
||||||
{
|
{
|
||||||
MinecraftAccountPtr account =
|
QMessageBox box(this);
|
||||||
MSALoginDialog::newAccount(this, tr("Please enter your Mojang account email and password to add your account."));
|
box.setWindowTitle(tr("Add account"));
|
||||||
|
box.setText(tr("How do you want to login?"));
|
||||||
|
box.setIcon(QMessageBox::Question);
|
||||||
|
auto deviceCode = box.addButton(tr("Using device code"), QMessageBox::ButtonRole::YesRole);
|
||||||
|
auto authCode = box.addButton(tr("Using auth code"), QMessageBox::ButtonRole::NoRole);
|
||||||
|
box.setDefaultButton(authCode);
|
||||||
|
box.exec();
|
||||||
|
MinecraftAccountPtr account = MSALoginDialog::newAccount(
|
||||||
|
this, tr("Please enter your Mojang account email and password to add your account."), box.clickedButton() == deviceCode);
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
m_accounts->addAccount(account);
|
m_accounts->addAccount(account);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user