Remove katabasis

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-05-15 12:53:06 +03:00
parent c2ed50627d
commit 80d8e3ee06
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
31 changed files with 99 additions and 1525 deletions

View File

@ -57,14 +57,14 @@ jobs:
qt_host: linux qt_host: linux
qt_arch: "" qt_arch: ""
qt_version: "5.12.8" qt_version: "5.12.8"
qt_modules: "" qt_modules: "qtnetworkauth"
- os: ubuntu-20.04 - os: ubuntu-20.04
qt_ver: 6 qt_ver: 6
qt_host: linux qt_host: linux
qt_arch: "" qt_arch: ""
qt_version: "6.2.4" qt_version: "6.2.4"
qt_modules: "qt5compat qtimageformats" qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022 - os: windows-2022
name: "Windows-MinGW-w64" name: "Windows-MinGW-w64"
@ -78,9 +78,9 @@ jobs:
vcvars_arch: "amd64" vcvars_arch: "amd64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: '' qt_arch: ""
qt_version: '6.7.0' qt_version: "6.7.0"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022 - os: windows-2022
name: "Windows-MSVC-arm64" name: "Windows-MSVC-arm64"
@ -89,18 +89,18 @@ jobs:
vcvars_arch: "amd64_arm64" vcvars_arch: "amd64_arm64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: 'win64_msvc2019_arm64' qt_arch: "win64_msvc2019_arm64"
qt_version: '6.7.0' qt_version: "6.7.0"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12 - os: macos-12
name: macOS name: macOS
macosx_deployment_target: 11.0 macosx_deployment_target: 11.0
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_arch: '' qt_arch: ""
qt_version: '6.7.0' qt_version: "6.7.0"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12 - os: macos-12
name: macOS-Legacy name: macOS-Legacy
@ -108,7 +108,7 @@ jobs:
qt_ver: 5 qt_ver: 5
qt_host: mac qt_host: mac
qt_version: "5.15.2" qt_version: "5.15.2"
qt_modules: "" qt_modules: "qtnetworkauth"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -150,6 +150,7 @@ jobs:
quazip-qt6:p quazip-qt6:p
ccache:p ccache:p
qt6-5compat:p qt6-5compat:p
qt6-networkauth:p
cmark:p cmark:p
- name: Force newer ccache - name: Force newer ccache

View File

@ -282,7 +282,7 @@ endif()
include(QtVersionlessBackport) include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5) if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(QT_VERSION_MAJOR 5) set(QT_VERSION_MAJOR 5)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml) find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth)
if(NOT Launcher_FORCE_BUNDLED_LIBS) if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt5 1.3 QUIET) find_package(QuaZip-Qt5 1.3 QUIET)
@ -296,7 +296,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6) set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat) find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS) if(NOT Launcher_FORCE_BUNDLED_LIBS)
@ -523,7 +523,6 @@ if(NOT cmark_FOUND)
else() else()
message(STATUS "Using system cmark") message(STATUS "Using system cmark")
endif() endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode) add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND) if (NOT ghc_filesystem_FOUND)

View File

@ -333,32 +333,6 @@
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## O2 (Katabasis fork)
Copyright (c) 2012, Akos Polster
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Gamemode ## Gamemode
Copyright (c) 2017-2022, Feral Interactive Copyright (c) 2017-2022, Feral Interactive

View File

@ -1234,7 +1234,6 @@ target_link_libraries(Launcher_logic
tomlplusplus::tomlplusplus tomlplusplus::tomlplusplus
qdcss qdcss
BuildConfig BuildConfig
Katabasis
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem ghcFilesystem::ghc_filesystem
) )
@ -1252,6 +1251,7 @@ target_link_libraries(Launcher_logic
Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::NetworkAuth
${Launcher_QT_LIBS} ${Launcher_QT_LIBS}
) )
target_link_libraries(Launcher_logic target_link_libraries(Launcher_logic
@ -1322,7 +1322,6 @@ if(Launcher_BUILD_UPDATER)
Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Network
${Launcher_QT_LIBS} ${Launcher_QT_LIBS}
cmark::cmark cmark::cmark
Katabasis
) )
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)

View File

@ -34,12 +34,43 @@
*/ */
#pragma once #pragma once
#include <katabasis/Bits.h>
#include <QByteArray> #include <QByteArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include <QVector> #include <QVector>
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariantMap>
namespace Katabasis {
enum class Activity {
Idle,
LoggingIn,
LoggingOut,
Refreshing,
FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated
FailedHard, //!< hard failure. auth is invalid
FailedGone, //!< hard failure. auth is invalid, and the account no longer exists
Succeeded
};
enum class Validity { None, Assumed, Certain };
struct Token {
QDateTime issueInstant;
QDateTime notAfter;
QString token;
QString refresh_token;
QVariantMap extra;
Validity validity = Validity::None;
bool persistent = true;
};
} // namespace Katabasis
struct Skin { struct Skin {
QString id; QString id;
QString url; QString url;

View File

@ -37,8 +37,6 @@ class AuthStep : public QObject {
signals: signals:
void finished(AccountTaskState resultingState, QString message); void finished(AccountTaskState resultingState, QString message);
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
protected: protected:
AccountData* m_data; AccountData* m_data;

View File

@ -21,6 +21,7 @@ void AuthFlow::succeed()
void AuthFlow::executeTask() void AuthFlow::executeTask()
{ {
if (m_currentStep) { if (m_currentStep) {
emitFailed("No task");
return; return;
} }
changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
@ -39,38 +40,10 @@ void AuthFlow::nextStep()
qDebug() << "AuthFlow:" << m_currentStep->describe(); qDebug() << "AuthFlow:" << m_currentStep->describe();
m_steps.pop_front(); m_steps.pop_front();
connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode);
connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode);
m_currentStep->perform(); m_currentStep->perform();
} }
QString AuthFlow::getStateMessage() const
{
switch (m_taskState) {
case AccountTaskState::STATE_CREATED:
return "Waiting...";
case AccountTaskState::STATE_WORKING:
if (m_currentStep)
return m_currentStep->describe();
return tr("Working...");
case AccountTaskState::STATE_SUCCEEDED:
return tr("Authentication task succeeded.");
case AccountTaskState::STATE_OFFLINE:
return tr("Failed to contact the authentication server.");
case AccountTaskState::STATE_DISABLED:
return tr("Client ID has changed. New session needs to be created.");
case AccountTaskState::STATE_FAILED_SOFT:
return tr("Encountered an error during authentication.");
case AccountTaskState::STATE_FAILED_HARD:
return tr("Failed to authenticate. The session has expired.");
case AccountTaskState::STATE_FAILED_GONE:
return tr("Failed to authenticate. The account no longer exists.");
default:
return tr("...");
}
}
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
{ {
if (changeState(resultingState, message)) { if (changeState(resultingState, message)) {
@ -81,54 +54,61 @@ void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
bool AuthFlow::changeState(AccountTaskState newState, QString reason) bool AuthFlow::changeState(AccountTaskState newState, QString reason)
{ {
m_taskState = newState; m_taskState = newState;
// FIXME: virtual method invoked in constructor. setDetails(reason);
// We want that behavior, but maybe make it less weird?
setStatus(getStateMessage());
switch (newState) { switch (newState) {
case AccountTaskState::STATE_CREATED: { case AccountTaskState::STATE_CREATED: {
setStatus(tr("Waiting..."));
m_data->errorString.clear(); m_data->errorString.clear();
return true; return true;
} }
case AccountTaskState::STATE_WORKING: { case AccountTaskState::STATE_WORKING: {
setStatus(m_currentStep ? m_currentStep->describe() : tr("Working..."));
m_data->accountState = AccountState::Working; m_data->accountState = AccountState::Working;
return true; return true;
} }
case AccountTaskState::STATE_SUCCEEDED: { case AccountTaskState::STATE_SUCCEEDED: {
setStatus(tr("Authentication task succeeded."));
m_data->accountState = AccountState::Online; m_data->accountState = AccountState::Online;
emitSucceeded(); emitSucceeded();
return false; return false;
} }
case AccountTaskState::STATE_OFFLINE: { case AccountTaskState::STATE_OFFLINE: {
setStatus(tr("Failed to contact the authentication server."));
m_data->errorString = reason; m_data->errorString = reason;
m_data->accountState = AccountState::Offline; m_data->accountState = AccountState::Offline;
emitFailed(reason); emitFailed(reason);
return false; return false;
} }
case AccountTaskState::STATE_DISABLED: { case AccountTaskState::STATE_DISABLED: {
setStatus(tr("Client ID has changed. New session needs to be created."));
m_data->errorString = reason; m_data->errorString = reason;
m_data->accountState = AccountState::Disabled; m_data->accountState = AccountState::Disabled;
emitFailed(reason); emitFailed(reason);
return false; return false;
} }
case AccountTaskState::STATE_FAILED_SOFT: { case AccountTaskState::STATE_FAILED_SOFT: {
setStatus(tr("Encountered an error during authentication."));
m_data->errorString = reason; m_data->errorString = reason;
m_data->accountState = AccountState::Errored; m_data->accountState = AccountState::Errored;
emitFailed(reason); emitFailed(reason);
return false; return false;
} }
case AccountTaskState::STATE_FAILED_HARD: { case AccountTaskState::STATE_FAILED_HARD: {
setStatus(tr("Failed to authenticate. The session has expired."));
m_data->errorString = reason; m_data->errorString = reason;
m_data->accountState = AccountState::Expired; m_data->accountState = AccountState::Expired;
emitFailed(reason); emitFailed(reason);
return false; return false;
} }
case AccountTaskState::STATE_FAILED_GONE: { case AccountTaskState::STATE_FAILED_GONE: {
setStatus(tr("Failed to authenticate. The account no longer exists."));
m_data->errorString = reason; m_data->errorString = reason;
m_data->accountState = AccountState::Gone; m_data->accountState = AccountState::Gone;
emitFailed(reason); emitFailed(reason);
return false; return false;
} }
default: { default: {
setStatus(tr("..."));
QString error = tr("Unknown account task state: %1").arg(int(newState)); QString error = tr("Unknown account task state: %1").arg(int(newState));
m_data->accountState = AccountState::Errored; m_data->accountState = AccountState::Errored;
emitFailed(error); emitFailed(error);

View File

@ -7,8 +7,6 @@
#include <QSet> #include <QSet>
#include <QVector> #include <QVector>
#include <katabasis/DeviceFlow.h>
#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "tasks/Task.h" #include "tasks/Task.h"
@ -27,18 +25,9 @@ class AuthFlow : public Task {
AccountTaskState taskState() { return m_taskState; } AccountTaskState taskState() { return m_taskState; }
signals: signals:
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
void activityChanged(Katabasis::Activity activity); void activityChanged(Katabasis::Activity activity);
protected: protected:
/**
* Returns the state message for the given state.
* Used to set the status message for the task.
* Should be overridden by subclasses that want to change messages for a given state.
*/
virtual QString getStateMessage() const;
void succeed(); void succeed();
void nextStep(); void nextStep();

View File

@ -35,28 +35,44 @@
#include "MSAStep.h" #include "MSAStep.h"
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
#include <QAbstractOAuth2>
#include <QDesktopServices>
#include <QNetworkRequest> #include <QNetworkRequest>
#include "Application.h" #include "Application.h"
#include "Logging.h"
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action)
{ {
m_clientId = APPLICATION->getMSAClientID(); m_clientId = APPLICATION->getMSAClientID();
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = m_clientId;
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
// FIXME: OAuth2 is not aware of our fancy shared pointers auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this);
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); oauth2.setReplyHandler(replyHandler);
oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
oauth2.setScope("XboxLive.SignIn XboxLive.offline_access");
oauth2.setClientIdentifier(m_clientId);
oauth2.setNetworkAccessManager(APPLICATION->network().get());
connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); m_data->msaClientID = oauth2.clientIdentifier();
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
m_data->msaToken.notAfter = oauth2.expirationAt();
m_data->msaToken.extra = oauth2.extraTokens();
m_data->msaToken.refresh_token = oauth2.refreshToken();
m_data->msaToken.token = oauth2.token();
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &QDesktopServices::openUrl);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
[this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this,
[this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; });
} }
QString MSAStep::describe() QString MSAStep::describe()
@ -69,68 +85,22 @@ void MSAStep::perform()
switch (m_action) { switch (m_action) {
case Refresh: { case Refresh: {
if (m_data->msaClientID != m_clientId) { if (m_data->msaClientID != m_clientId) {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_DISABLED, emit finished(AccountTaskState::STATE_DISABLED,
tr("Microsoft user authentication failed - client identification has changed.")); tr("Microsoft user authentication failed - client identification has changed."));
} }
m_oauth2->refresh(); oauth2.setRefreshToken(m_data->msaToken.refresh_token);
oauth2.refreshAccessToken();
return; return;
} }
case Login: { case Login: {
QVariantMap extraOpts; oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) {
extraOpts["prompt"] = "select_account"; map->insert("prompt", "select_account");
m_oauth2->setExtraRequestParams(extraOpts); map->insert("cobrandid", "8058f65d-ce06-4c30-9559-473c9275a65d");
});
*m_data = AccountData(); *m_data = AccountData();
m_data->msaClientID = m_clientId; m_data->msaClientID = m_clientId;
m_oauth2->login(); oauth2.grant();
return;
}
}
}
void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity)
{
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();
QVariantMap extraTokens = m_oauth2->extraTokens();
if (!extraTokens.isEmpty()) {
qCDebug(authCredentials()) << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key);
}
}
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
return;
}
case Katabasis::Activity::FailedSoft: {
// NOTE: soft error in the first step means 'offline'
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error."));
return;
}
case Katabasis::Activity::FailedGone: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists."));
return;
}
case Katabasis::Activity::FailedHard: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
return;
}
default: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
return; return;
} }
} }

View File

@ -36,11 +36,9 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include <katabasis/DeviceFlow.h> #include <QtNetworkAuth/qoauth2authorizationcodeflow.h>
class MSAStep : public AuthStep { class MSAStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
@ -54,11 +52,8 @@ class MSAStep : public AuthStep {
QString describe() override; QString describe() override;
private slots:
void onOAuthActivityChanged(Katabasis::Activity activity);
private: private:
Katabasis::DeviceFlow* m_oauth2 = nullptr;
Action m_action; Action m_action;
QString m_clientId; QString m_clientId;
QOAuth2AuthorizationCodeFlow oauth2;
}; };

View File

@ -5,7 +5,6 @@
qt.*.debug=false qt.*.debug=false
# don't log credentials by default # don't log credentials by default
launcher.auth.credentials.debug=false launcher.auth.credentials.debug=false
katabasis.*.debug=false
# remove the debug lines, other log levels still get through # remove the debug lines, other log levels still get through
launcher.task.net.download.debug=false launcher.task.net.download.debug=false
# enable or disable whole catageries # enable or disable whole catageries

View File

@ -67,9 +67,6 @@ 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(), &AuthFlow::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode);
connect(m_loginTask.get(), &AuthFlow::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode);
connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick);
m_loginTask->start(); m_loginTask->start();
return QDialog::exec(); return QDialog::exec();
@ -80,55 +77,6 @@ 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);
if (urlString == "https://www.microsoft.com/link" && !code.isEmpty()) {
urlString += QString("?otc=%1").arg(code);
DesktopServices::openUrl(urlString);
ui->label->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->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));
}
ui->actionButton->setVisible(true);
connect(ui->actionButton, &QPushButton::clicked, [=]() {
DesktopServices::openUrl(uri);
QClipboard* cb = QApplication::clipboard();
cb->setText(code);
});
}
void MSALoginDialog::hideVerificationUriAndCode()
{
m_externalLoginTimer.stop();
ui->actionButton->setVisible(false);
}
void MSALoginDialog::setUserInputsEnabled(bool enable) void MSALoginDialog::setUserInputsEnabled(bool enable)
{ {
ui->buttonBox->setEnabled(enable); ui->buttonBox->setEnabled(enable);

View File

@ -15,7 +15,6 @@
#pragma once #pragma once
#include <QTimer>
#include <QtCore/QEventLoop> #include <QtCore/QEventLoop>
#include <QtWidgets/QDialog> #include <QtWidgets/QDialog>
@ -45,16 +44,9 @@ class MSALoginDialog : public QDialog {
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;
shared_qobject_ptr<AuthFlow> m_loginTask; shared_qobject_ptr<AuthFlow> m_loginTask;
QTimer m_externalLoginTimer;
int m_externalLoginElapsed = 0;
int m_externalLoginTimeout = 0;
}; };

View File

@ -32,14 +32,6 @@ Simple Java tool that prints the JVM details - version and platform bitness.
Do what you want with it. It is so trivial that noone cares. Do what you want with it. It is so trivial that noone cares.
## Katabasis
Oauth2 library customized for Microsoft authentication.
This is a fork of the [O2 library](https://github.com/pipacs/o2).
MIT licensed.
## launcher ## launcher
Java launcher part for Minecraft. Java launcher part for Minecraft.

View File

@ -1,2 +0,0 @@
build/
*.kdev4

View File

@ -1,58 +0,0 @@
cmake_minimum_required(VERSION 3.9.4)
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
if(IS_IN_SOURCE_BUILD)
message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.")
endif()
project(Katabasis)
enable_testing()
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD_REQUIRED true)
set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_C_STANDARD 11)
if(QT_VERSION_MAJOR EQUAL 5)
find_package(Qt5 COMPONENTS Core Network REQUIRED)
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
find_package(Qt6 COMPONENTS Core Network REQUIRED)
endif()
set( katabasis_PRIVATE
src/DeviceFlow.cpp
src/JsonResponse.cpp
src/JsonResponse.h
src/PollServer.cpp
src/Reply.cpp
)
set( katabasis_PUBLIC
include/katabasis/DeviceFlow.h
include/katabasis/Globals.h
include/katabasis/PollServer.h
include/katabasis/Reply.h
include/katabasis/RequestParameter.h
)
ecm_qt_declare_logging_category(katabasis_PRIVATE
HEADER KatabasisLogging.h # NOTE: this won't be in src/, but CMAKE_BINARY_DIR/src isn't included by default so this should be fine
IDENTIFIER katabasisCredentials
CATEGORY_NAME "katabasis.credentials"
DEFAULT_SEVERITY Warning
DESCRIPTION "Secrets and credentials from Katabasis"
EXPORT "Katabasis"
)
add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} )
target_link_libraries(Katabasis Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
# needed for statically linked Katabasis in shared libs on x86_64
set_target_properties(Katabasis
PROPERTIES POSITION_INDEPENDENT_CODE TRUE
)
target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis)

View File

@ -1,23 +0,0 @@
Copyright (c) 2012, Akos Polster
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,36 +0,0 @@
# Katabasis - MS-flavored OAuth for Qt, derived from the O2 library
This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful.
It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored.
[You can find the original library's git repository here.](https://github.com/pipacs/o2)
Notes to contributors:
* Please follow the coding style of the existing source, where reasonable
* Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
* If you are interested in working on this, come to the Prism Launcher Discord server and talk first
## Installation
Clone the Github repository, integrate the it into your CMake build system.
The library is static only, dynamic linking and system-wide installation are out of scope and undesirable.
## Usage
At this stage, don't, unless you want to help with the library itself.
This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features:
* Multiple accounts
* Multi-stage authentication/authorization schemes
* Tighter control over token chains and their storage
* Talking to complex APIs and individually authorized microservices
* Token lifetime management, 'offline mode' and resilience in face of network failures
* Token and claims/entitlements validation
* Caching of some API results
* XBox magic
* Mojang magic
* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available)

View File

@ -1,108 +0,0 @@
## O2 library by Akos Polster and contributors
[The origin of this fork.](https://github.com/pipacs/o2)
> Copyright (c) 2012, Akos Polster
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright notice, this
> list of conditions and the following disclaimer.
>
> * Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## SimpleCrypt by Andre Somers
Cryptographic methods for Qt.
> Copyright (c) 2011, Andre Somers
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
> * Redistributions in binary form must reproduce the above copyright
> notice, this list of conditions and the following disclaimer in the
> documentation and/or other materials provided with the distribution.
> * Neither the name of the Rathenau Instituut, Andre Somers nor the
> names of its contributors may be used to endorse or promote products
> derived from this software without specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY
> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
Configurable settings storage, Twitter XAuth specialization, new demos, cleanups.
> "Hi Akos,
>
> I'm writing this mail to confirm that my contributions to the O2 library, available here <https://github.com/pipacs/o2>, can be freely distributed according to the project's license (as shown in the LICENSE file).
>
> Regards,
> -mandeep"
## Sergey Gavrushkin <https://github.com/ncux>
FreshBooks specialization
## Theofilos Intzoglou <https://github.com/parapente>
Hubic specialization
## Dimitar
SurveyMonkey specialization
## David Brooks <https://github.com/dbrnz>
CMake related fixes and improvements.
## Lukas Vogel <https://github.com/lukedirtwalker>
Spotify support
## Alan Garny <https://github.com/agarny>
Windows DLL build support
## MartinMikita <https://github.com/MartinMikita>
Bug fixes
## Larry Shaffer <https://github.com/dakcarto>
Versioning, shared lib, install target and header support
## Gilmanov Ildar <https://github.com/gilmanov-ildar>
Bug fixes, support for ```qml``` module
## Fabian Vogt <https://github.com/Vogtinator>
Bug fixes, support for building without Qt keywords enabled

View File

@ -1,33 +0,0 @@
#pragma once
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariantMap>
namespace Katabasis {
enum class Activity {
Idle,
LoggingIn,
LoggingOut,
Refreshing,
FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated
FailedHard, //!< hard failure. auth is invalid
FailedGone, //!< hard failure. auth is invalid, and the account no longer exists
Succeeded
};
enum class Validity { None, Assumed, Certain };
struct Token {
QDateTime issueInstant;
QDateTime notAfter;
QString token;
QString refresh_token;
QVariantMap extra;
Validity validity = Validity::None;
bool persistent = true;
};
} // namespace Katabasis

View File

@ -1,149 +0,0 @@
#pragma once
#include <QLoggingCategory>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPair>
#include "Bits.h"
#include "Reply.h"
#include "RequestParameter.h"
namespace Katabasis {
class ReplyServer;
class PollServer;
/// Simple OAuth2 Device Flow authenticator.
class DeviceFlow : public QObject {
Q_OBJECT
public:
Q_ENUMS(GrantFlow)
public:
struct Options {
QString userAgent = QStringLiteral("Katabasis/1.0");
QString responseType = QStringLiteral("code");
QString scope;
QString clientIdentifier;
QString clientSecret;
QUrl authorizationUrl;
QUrl accessTokenUrl;
};
public:
/// Are we authenticated?
bool linked();
/// Authentication token.
QString token();
/// Provider-specific extra tokens, available after a successful authentication
QVariantMap extraTokens();
public:
// TODO: put in `Options`
/// User-defined extra parameters to append to request URL
QVariantMap extraRequestParams();
void setExtraRequestParams(const QVariantMap& value);
// TODO: split up the class into multiple, each implementing one OAuth2 flow
/// Grant type (if non-standard)
QString grantType();
void setGrantType(const QString& value);
public:
/// Constructor.
/// @param parent Parent object.
explicit DeviceFlow(Options& opts, Token& token, QObject* parent = 0, QNetworkAccessManager* manager = 0);
/// Get refresh token.
QString refreshToken();
/// Get token expiration time
QDateTime expires();
public slots:
/// Authenticate.
void login();
/// De-authenticate.
void logout();
/// Refresh token.
bool refresh();
/// Handle situation where reply server has opted to close its connection
void serverHasClosed(bool paramsfound = false);
signals:
/// Emitted when client needs to open a web browser window, with the given URL.
void openBrowser(const QUrl& url);
/// Emitted when client can close the browser window.
void closeBrowser();
/// Emitted when client needs to show a verification uri and user code
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
/// Emitted when the internal state changes
void activityChanged(Activity activity);
public slots:
/// Handle verification response.
void onVerificationReceived(QMap<QString, QString>);
protected slots:
/// Handle completion of a Device Authorization Request
void onDeviceAuthReplyFinished();
/// Handle completion of a refresh request.
void onRefreshFinished();
/// Handle failure of a refresh request.
void onRefreshError(QNetworkReply::NetworkError error, QNetworkReply* reply);
protected:
/// Set refresh token.
void setRefreshToken(const QString& v);
/// Set token expiration time.
void setExpires(QDateTime v);
/// Start polling authorization server
void startPollServer(const QVariantMap& params, int expiresIn);
/// Set authentication token.
void setToken(const QString& v);
/// Set the linked state
void setLinked(bool v);
/// Set extra tokens found in OAuth response
void setExtraTokens(QVariantMap extraTokens);
/// Set local poll server
void setPollServer(PollServer* server);
PollServer* pollServer() const;
void updateActivity(Activity activity);
protected:
Options options_;
QVariantMap extraReqParams_;
QNetworkAccessManager* manager_ = nullptr;
ReplyList timedReplies_;
QString grantType_;
protected:
Token& token_;
private:
PollServer* pollServer_ = nullptr;
Activity activity_ = Activity::Idle;
};
} // namespace Katabasis

View File

@ -1,59 +0,0 @@
#pragma once
namespace Katabasis {
// Common constants
const char ENCRYPTION_KEY[] = "12345678";
const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded";
const char MIME_TYPE_JSON[] = "application/json";
// OAuth 1/1.1 Request Parameters
const char OAUTH_CALLBACK[] = "oauth_callback";
const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key";
const char OAUTH_NONCE[] = "oauth_nonce";
const char OAUTH_SIGNATURE[] = "oauth_signature";
const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method";
const char OAUTH_TIMESTAMP[] = "oauth_timestamp";
const char OAUTH_VERSION[] = "oauth_version";
// OAuth 1/1.1 Response Parameters
const char OAUTH_TOKEN[] = "oauth_token";
const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret";
const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed";
const char OAUTH_VERFIER[] = "oauth_verifier";
// OAuth 2 Request Parameters
const char OAUTH2_RESPONSE_TYPE[] = "response_type";
const char OAUTH2_CLIENT_ID[] = "client_id";
const char OAUTH2_CLIENT_SECRET[] = "client_secret";
const char OAUTH2_USERNAME[] = "username";
const char OAUTH2_PASSWORD[] = "password";
const char OAUTH2_REDIRECT_URI[] = "redirect_uri";
const char OAUTH2_SCOPE[] = "scope";
const char OAUTH2_GRANT_TYPE_CODE[] = "code";
const char OAUTH2_GRANT_TYPE_TOKEN[] = "token";
const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password";
const char OAUTH2_GRANT_TYPE_DEVICE[] = "urn:ietf:params:oauth:grant-type:device_code";
const char OAUTH2_GRANT_TYPE[] = "grant_type";
const char OAUTH2_API_KEY[] = "api_key";
const char OAUTH2_STATE[] = "state";
const char OAUTH2_CODE[] = "code";
// OAuth 2 Response Parameters
const char OAUTH2_ACCESS_TOKEN[] = "access_token";
const char OAUTH2_REFRESH_TOKEN[] = "refresh_token";
const char OAUTH2_EXPIRES_IN[] = "expires_in";
const char OAUTH2_DEVICE_CODE[] = "device_code";
const char OAUTH2_USER_CODE[] = "user_code";
const char OAUTH2_VERIFICATION_URI[] = "verification_uri";
const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in
const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete";
const char OAUTH2_INTERVAL[] = "interval";
// Parameter values
const char AUTHORIZATION_CODE[] = "authorization_code";
// Standard HTTP headers
const char HTTP_HTTP_HEADER[] = "HTTP";
const char HTTP_AUTHORIZATION_HEADER[] = "Authorization";
} // namespace Katabasis

View File

@ -1,51 +0,0 @@
#pragma once
#include <QByteArray>
#include <QMap>
#include <QNetworkRequest>
#include <QObject>
#include <QString>
#include <QTimer>
class QNetworkAccessManager;
namespace Katabasis {
/// Poll an authorization server for token
class PollServer : public QObject {
Q_OBJECT
public:
explicit PollServer(QNetworkAccessManager* manager,
const QNetworkRequest& request,
const QByteArray& payload,
int expiresIn,
QObject* parent = 0);
/// Seconds to wait between polling requests
Q_PROPERTY(int interval READ interval WRITE setInterval)
int interval() const;
void setInterval(int interval);
signals:
void verificationReceived(QMap<QString, QString>);
void serverClosed(bool); // whether it has found parameters
public slots:
void startPolling();
protected slots:
void onPollTimeout();
void onExpiration();
void onReplyFinished();
protected:
QNetworkAccessManager* manager_;
const QNetworkRequest request_;
const QByteArray payload_;
const int expiresIn_;
QTimer expirationTimer;
QTimer pollTimer;
};
} // namespace Katabasis

View File

@ -1,63 +0,0 @@
#pragma once
#include <QByteArray>
#include <QList>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
namespace Katabasis {
constexpr int defaultTimeout = 30 * 1000;
/// A network request/reply pair that can time out.
class Reply : public QTimer {
Q_OBJECT
public:
Reply(QNetworkReply* reply, int timeOut = defaultTimeout, QObject* parent = 0);
signals:
void error(QNetworkReply::NetworkError);
public slots:
/// When time out occurs, the QNetworkReply's error() signal is triggered.
void onTimeOut();
public:
QNetworkReply* reply;
bool timedOut = false;
};
/// List of O2Replies.
class ReplyList {
public:
ReplyList() { ignoreSslErrors_ = false; }
/// Destructor.
/// Deletes all O2Reply instances in the list.
virtual ~ReplyList();
/// Create a new O2Reply from a QNetworkReply, and add it to this list.
void add(QNetworkReply* reply, int timeOut = defaultTimeout);
/// Add an O2Reply to the list, while taking ownership of it.
void add(Reply* reply);
/// Remove item from the list that corresponds to a QNetworkReply.
void remove(QNetworkReply* reply);
/// Find an O2Reply in the list, corresponding to a QNetworkReply.
/// @return Matching O2Reply or NULL.
Reply* find(QNetworkReply* reply);
bool ignoreSslErrors();
void setIgnoreSslErrors(bool ignoreSslErrors);
protected:
QList<Reply*> replies_;
bool ignoreSslErrors_;
};
} // namespace Katabasis

View File

@ -1,13 +0,0 @@
#pragma once
namespace Katabasis {
/// Request parameter (name-value pair) participating in authentication.
struct RequestParameter {
RequestParameter(const QByteArray& n, const QByteArray& v) : name(n), value(v) {}
bool operator<(const RequestParameter& other) const { return (name == other.name) ? (value < other.value) : (name < other.name); }
QByteArray name;
QByteArray value;
};
} // namespace Katabasis

View File

@ -1,467 +0,0 @@
#include <QCryptographicHash>
#include <QDataStream>
#include <QDateTime>
#include <QDebug>
#include <QList>
#include <QMap>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPair>
#include <QTcpServer>
#include <QTimer>
#include <QUuid>
#include <QVariantMap>
#include <QUrlQuery>
#include "katabasis/DeviceFlow.h"
#include "katabasis/Globals.h"
#include "katabasis/PollServer.h"
#include "JsonResponse.h"
#include "KatabasisLogging.h"
namespace {
// ref: https://tools.ietf.org/html/rfc8628#section-3.2
// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both.
bool hasMandatoryDeviceAuthParams(const QVariantMap& params)
{
if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE))
return false;
if (!params.contains(Katabasis::OAUTH2_USER_CODE))
return false;
if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL)))
return false;
if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN))
return false;
return true;
}
QByteArray createQueryParameters(const QList<Katabasis::RequestParameter>& parameters)
{
QByteArray ret;
bool first = true;
for (auto& h : parameters) {
if (first) {
first = false;
} else {
ret.append("&");
}
ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value));
}
return ret;
}
} // namespace
namespace Katabasis {
DeviceFlow::DeviceFlow(Options& opts, Token& token, QObject* parent, QNetworkAccessManager* manager) : QObject(parent), token_(token)
{
manager_ = manager ? manager : new QNetworkAccessManager(this);
qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
options_ = opts;
}
bool DeviceFlow::linked()
{
return token_.validity != Validity::None;
}
void DeviceFlow::setLinked(bool v)
{
qDebug() << "DeviceFlow::setLinked:" << (v ? "true" : "false");
token_.validity = v ? Validity::Certain : Validity::None;
}
void DeviceFlow::updateActivity(Activity activity)
{
if (activity_ == activity) {
return;
}
activity_ = activity;
switch (activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::LoggingOut:
case Katabasis::Activity::Refreshing:
// non-terminal states...
break;
case Katabasis::Activity::FailedSoft:
// terminal state, tokens did not change
break;
case Katabasis::Activity::FailedHard:
case Katabasis::Activity::FailedGone:
// terminal state, tokens are invalid
token_ = Token();
break;
case Katabasis::Activity::Succeeded:
setLinked(true);
break;
}
emit activityChanged(activity_);
}
QString DeviceFlow::token()
{
return token_.token;
}
void DeviceFlow::setToken(const QString& v)
{
token_.token = v;
}
QVariantMap DeviceFlow::extraTokens()
{
return token_.extra;
}
void DeviceFlow::setExtraTokens(QVariantMap extraTokens)
{
token_.extra = extraTokens;
}
void DeviceFlow::setPollServer(PollServer* server)
{
if (pollServer_)
pollServer_->deleteLater();
pollServer_ = server;
}
PollServer* DeviceFlow::pollServer() const
{
return pollServer_;
}
QVariantMap DeviceFlow::extraRequestParams()
{
return extraReqParams_;
}
void DeviceFlow::setExtraRequestParams(const QVariantMap& value)
{
extraReqParams_ = value;
}
QString DeviceFlow::grantType()
{
if (!grantType_.isEmpty())
return grantType_;
return OAUTH2_GRANT_TYPE_DEVICE;
}
void DeviceFlow::setGrantType(const QString& value)
{
grantType_ = value;
}
// First get the URL and token to display to the user
void DeviceFlow::login()
{
qDebug() << "DeviceFlow::link";
updateActivity(Activity::LoggingIn);
setLinked(false);
setToken("");
setExtraTokens(QVariantMap());
setRefreshToken(QString());
setExpires(QDateTime());
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
QByteArray payload = createQueryParameters(parameters);
QUrl url(options_.authorizationUrl);
QNetworkRequest deviceRequest(url);
deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply* tokenReply = manager_->post(deviceRequest, payload);
connect(tokenReply, &QNetworkReply::finished, this, &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection);
}
// Then, once we get them, present them to the user
void DeviceFlow::onDeviceAuthReplyFinished()
{
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished";
QNetworkReply* tokenReply = qobject_cast<QNetworkReply*>(sender());
if (!tokenReply) {
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null";
return;
}
if (tokenReply->error() == QNetworkReply::NoError) {
QByteArray replyData = tokenReply->readAll();
// Dump replyData
// SENSITIVE DATA in RelWithDebInfo or Debug builds
// qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n";
// qDebug() << QString( replyData );
QVariantMap params = parseJsonResponse(replyData);
// Dump tokens
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n";
foreach (QString key, params.keys()) {
// SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first
qDebug() << key << ": " << params.value(key).toString();
}
// Check for mandatory parameters
if (hasMandatoryDeviceAuthParams(params)) {
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device auth request response";
const QString userCode = params.take(OAUTH2_USER_CODE).toString();
QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl();
if (uri.isEmpty())
uri = params.take(OAUTH2_VERIFICATION_URL).toUrl();
if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl());
bool ok = false;
int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok);
if (!ok) {
qWarning() << "DeviceFlow::startPollServer: No expired_in parameter";
updateActivity(Activity::FailedHard);
return;
}
emit showVerificationUriAndCode(uri, userCode, expiresIn);
startPollServer(params, expiresIn);
} else {
qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Mandatory parameters missing from response";
updateActivity(Activity::FailedHard);
}
}
tokenReply->deleteLater();
}
// Spin up polling for the user completing the login flow out of band
void DeviceFlow::startPollServer(const QVariantMap& params, int expiresIn)
{
qDebug() << "DeviceFlow::startPollServer: device_ and user_code expires in" << expiresIn << "seconds";
QUrl url(options_.accessTokenUrl);
QNetworkRequest authRequest(url);
authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString();
const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_;
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
if (!options_.clientSecret.isEmpty()) {
parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8()));
}
parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8()));
parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8()));
QByteArray payload = createQueryParameters(parameters);
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);
}
}
connect(pollServer, &PollServer::verificationReceived, this, &DeviceFlow::onVerificationReceived);
connect(pollServer, &PollServer::serverClosed, this, &DeviceFlow::serverHasClosed);
setPollServer(pollServer);
pollServer->startPolling();
}
// Once the user completes the flow, update the internal state and report it to observers
void DeviceFlow::onVerificationReceived(const QMap<QString, QString> response)
{
qDebug() << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()";
emit closeBrowser();
if (response.contains("error")) {
qWarning() << "DeviceFlow::onVerificationReceived: Verification failed:" << response;
updateActivity(Activity::FailedHard);
return;
}
// Check for mandatory tokens
if (response.contains(OAUTH2_ACCESS_TOKEN)) {
qDebug() << "DeviceFlow::onVerificationReceived: Access token returned for implicit or device flow";
setToken(response.value(OAUTH2_ACCESS_TOKEN));
if (response.contains(OAUTH2_EXPIRES_IN)) {
bool ok = false;
int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok);
if (ok) {
qDebug() << "DeviceFlow::onVerificationReceived: Token expires in" << expiresIn << "seconds";
setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
}
}
if (response.contains(OAUTH2_REFRESH_TOKEN)) {
setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
}
updateActivity(Activity::Succeeded);
} else {
qWarning() << "DeviceFlow::onVerificationReceived: Access token missing from response for implicit or device flow";
updateActivity(Activity::FailedHard);
}
}
// Or if the flow fails or the polling times out, update the internal state with error and report it to observers
void DeviceFlow::serverHasClosed(bool paramsfound)
{
if (!paramsfound) {
// server has probably timed out after receiving first response
updateActivity(Activity::FailedHard);
}
// poll server is not re-used for later auth requests
setPollServer(NULL);
}
void DeviceFlow::logout()
{
qDebug() << "DeviceFlow::unlink";
updateActivity(Activity::LoggingOut);
// FIXME: implement logout flows... if they exist
token_ = Token();
updateActivity(Activity::FailedHard);
}
QDateTime DeviceFlow::expires()
{
return token_.notAfter;
}
void DeviceFlow::setExpires(QDateTime v)
{
token_.notAfter = v;
}
QString DeviceFlow::refreshToken()
{
return token_.refresh_token;
}
void DeviceFlow::setRefreshToken(const QString& v)
{
qCDebug(katabasisCredentials) << "new refresh token:" << v;
token_.refresh_token = v;
}
namespace {
QByteArray buildRequestBody(const QMap<QString, QString>& parameters)
{
QByteArray body;
bool first = true;
foreach (QString key, parameters.keys()) {
if (first) {
first = false;
} else {
body.append("&");
}
QString value = parameters.value(key);
body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value));
}
return body;
}
} // namespace
bool DeviceFlow::refresh()
{
qDebug() << "DeviceFlow::refresh: Token: ..." << refreshToken().right(7);
updateActivity(Activity::Refreshing);
if (refreshToken().isEmpty()) {
qWarning() << "DeviceFlow::refresh: No refresh token";
onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr);
return false;
}
if (options_.accessTokenUrl.isEmpty()) {
qWarning() << "DeviceFlow::refresh: Refresh token URL not set";
onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr);
return false;
}
QNetworkRequest refreshRequest(options_.accessTokenUrl);
refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM);
QMap<QString, QString> parameters;
parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
if (!options_.clientSecret.isEmpty()) {
parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
}
parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken());
parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN);
QByteArray data = buildRequestBody(parameters);
QNetworkReply* refreshReply = manager_->post(refreshRequest, data);
timedReplies_.add(refreshReply);
connect(refreshReply, &QNetworkReply::finished, this, &DeviceFlow::onRefreshFinished, Qt::QueuedConnection);
return true;
}
void DeviceFlow::onRefreshFinished()
{
QNetworkReply* refreshReply = qobject_cast<QNetworkReply*>(sender());
auto networkError = refreshReply->error();
if (networkError == QNetworkReply::NoError) {
QByteArray reply = refreshReply->readAll();
QVariantMap tokens = parseJsonResponse(reply);
setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString());
setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt()));
QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString();
if (!refreshToken.isEmpty()) {
setRefreshToken(refreshToken);
} else {
qDebug() << "No new refresh token. Keep the old one.";
}
timedReplies_.remove(refreshReply);
refreshReply->deleteLater();
updateActivity(Activity::Succeeded);
qDebug() << "New token expires in" << expires() << "seconds";
} else {
// FIXME: differentiate the error more here
onRefreshError(networkError, refreshReply);
}
}
void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error, QNetworkReply* refreshReply)
{
QString errorString = "No Reply";
if (refreshReply) {
timedReplies_.remove(refreshReply);
errorString = refreshReply->errorString();
}
switch (error) {
// used for invalid credentials and similar errors. Fall through.
case QNetworkReply::AuthenticationRequiredError:
case QNetworkReply::ContentAccessDenied:
case QNetworkReply::ContentOperationNotPermittedError:
case QNetworkReply::ProtocolInvalidOperationError:
updateActivity(Activity::FailedHard);
break;
case QNetworkReply::ContentGoneError: {
updateActivity(Activity::FailedGone);
break;
}
case QNetworkReply::TimeoutError:
case QNetworkReply::OperationCanceledError:
case QNetworkReply::SslHandshakeFailedError:
default:
updateActivity(Activity::FailedSoft);
return;
}
if (refreshReply) {
refreshReply->deleteLater();
}
qDebug() << "DeviceFlow::onRefreshFinished: Error" << static_cast<int>(error) << " - " << errorString;
}
} // namespace Katabasis

View File

@ -1,27 +0,0 @@
#include "JsonResponse.h"
#include <QByteArray>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
namespace Katabasis {
QVariantMap parseJsonResponse(const QByteArray& data)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString();
return QVariantMap();
}
if (!doc.isObject()) {
qWarning() << "parseTokenResponse: Token response is not an object";
return QVariantMap();
}
return doc.object().toVariantMap();
}
} // namespace Katabasis

View File

@ -1,12 +0,0 @@
#pragma once
#include <QVariantMap>
class QByteArray;
namespace Katabasis {
/// Parse JSON data into a QVariantMap
QVariantMap parseJsonResponse(const QByteArray& data);
} // namespace Katabasis

View File

@ -1,118 +0,0 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "JsonResponse.h"
#include "katabasis/PollServer.h"
namespace {
QMap<QString, QString> toVerificationParams(const QVariantMap& map)
{
QMap<QString, QString> params;
for (QVariantMap::const_iterator i = map.constBegin(); i != map.constEnd(); ++i) {
params[i.key()] = i.value().toString();
}
return params;
}
} // namespace
namespace Katabasis {
PollServer::PollServer(QNetworkAccessManager* manager,
const QNetworkRequest& request,
const QByteArray& payload,
int expiresIn,
QObject* parent)
: QObject(parent), manager_(manager), request_(request), payload_(payload), expiresIn_(expiresIn)
{
expirationTimer.setTimerType(Qt::VeryCoarseTimer);
expirationTimer.setInterval(expiresIn * 1000);
expirationTimer.setSingleShot(true);
connect(&expirationTimer, SIGNAL(timeout()), this, SLOT(onExpiration()));
expirationTimer.start();
pollTimer.setTimerType(Qt::VeryCoarseTimer);
pollTimer.setInterval(5 * 1000);
pollTimer.setSingleShot(true);
connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout()));
}
int PollServer::interval() const
{
return pollTimer.interval() / 1000;
}
void PollServer::setInterval(int interval)
{
pollTimer.setInterval(interval * 1000);
}
void PollServer::startPolling()
{
if (expirationTimer.isActive()) {
pollTimer.start();
}
}
void PollServer::onPollTimeout()
{
qDebug() << "PollServer::onPollTimeout: retrying";
QNetworkReply* reply = manager_->post(request_, payload_);
connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished()));
}
void PollServer::onExpiration()
{
pollTimer.stop();
emit serverClosed(false);
}
void PollServer::onReplyFinished()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply) {
qDebug() << "PollServer::onReplyFinished: reply is null";
return;
}
QByteArray replyData = reply->readAll();
QMap<QString, QString> params = toVerificationParams(parseJsonResponse(replyData));
// Dump replyData
// SENSITIVE DATA in RelWithDebInfo or Debug builds
// qDebug() << "PollServer::onReplyFinished: replyData\n";
// qDebug() << QString( replyData );
if (reply->error() == QNetworkReply::TimeoutError) {
// rfc8628#section-3.2
// "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."
setInterval(interval() * 2);
pollTimer.start();
} else {
QString error = params.value("error");
if (error == "slow_down") {
// rfc8628#section-3.2
// "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."
setInterval(interval() + 5);
pollTimer.start();
} else if (error == "authorization_pending") {
// keep trying - rfc8628#section-3.2
// "The authorization request is still pending as the end user hasn't
// yet completed the user-interaction steps (Section 3.3)."
pollTimer.start();
} else {
expirationTimer.stop();
emit serverClosed(true);
// let O2 handle the other cases
emit verificationReceived(params);
}
}
reply->deleteLater();
}
} // namespace Katabasis

View File

@ -1,74 +0,0 @@
#include <QNetworkReply>
#include <QTimer>
#include "katabasis/Reply.h"
namespace Katabasis {
Reply::Reply(QNetworkReply* r, int timeOut, QObject* parent) : QTimer(parent), reply(r)
{
setSingleShot(true);
connect(this, &Reply::timeout, this, &Reply::onTimeOut, Qt::QueuedConnection);
start(timeOut);
}
void Reply::onTimeOut()
{
timedOut = true;
reply->abort();
}
// ----------------------------
ReplyList::~ReplyList()
{
foreach (Reply* timedReply, replies_) {
delete timedReply;
}
}
void ReplyList::add(QNetworkReply* reply, int timeOut)
{
if (reply && ignoreSslErrors()) {
reply->ignoreSslErrors();
}
add(new Reply(reply, timeOut));
}
void ReplyList::add(Reply* reply)
{
replies_.append(reply);
}
void ReplyList::remove(QNetworkReply* reply)
{
Reply* o2Reply = find(reply);
if (o2Reply) {
o2Reply->stop();
(void)replies_.removeOne(o2Reply);
// we took ownership, we must free
delete o2Reply;
}
}
Reply* ReplyList::find(QNetworkReply* reply)
{
foreach (Reply* timedReply, replies_) {
if (timedReply->reply == reply) {
return timedReply;
}
}
return 0;
}
bool ReplyList::ignoreSslErrors()
{
return ignoreSslErrors_;
}
void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors)
{
ignoreSslErrors_ = ignoreSslErrors;
}
} // namespace Katabasis