Remove katabasis
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
parent
c2ed50627d
commit
80d8e3ee06
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@ -57,14 +57,14 @@ jobs:
|
||||
qt_host: linux
|
||||
qt_arch: ""
|
||||
qt_version: "5.12.8"
|
||||
qt_modules: ""
|
||||
qt_modules: "qtnetworkauth"
|
||||
|
||||
- os: ubuntu-20.04
|
||||
qt_ver: 6
|
||||
qt_host: linux
|
||||
qt_arch: ""
|
||||
qt_version: "6.2.4"
|
||||
qt_modules: "qt5compat qtimageformats"
|
||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||
|
||||
- os: windows-2022
|
||||
name: "Windows-MinGW-w64"
|
||||
@ -78,9 +78,9 @@ jobs:
|
||||
vcvars_arch: "amd64"
|
||||
qt_ver: 6
|
||||
qt_host: windows
|
||||
qt_arch: ''
|
||||
qt_version: '6.7.0'
|
||||
qt_modules: 'qt5compat qtimageformats'
|
||||
qt_arch: ""
|
||||
qt_version: "6.7.0"
|
||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||
|
||||
- os: windows-2022
|
||||
name: "Windows-MSVC-arm64"
|
||||
@ -89,18 +89,18 @@ jobs:
|
||||
vcvars_arch: "amd64_arm64"
|
||||
qt_ver: 6
|
||||
qt_host: windows
|
||||
qt_arch: 'win64_msvc2019_arm64'
|
||||
qt_version: '6.7.0'
|
||||
qt_modules: 'qt5compat qtimageformats'
|
||||
qt_arch: "win64_msvc2019_arm64"
|
||||
qt_version: "6.7.0"
|
||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||
|
||||
- os: macos-12
|
||||
name: macOS
|
||||
macosx_deployment_target: 11.0
|
||||
qt_ver: 6
|
||||
qt_host: mac
|
||||
qt_arch: ''
|
||||
qt_version: '6.7.0'
|
||||
qt_modules: 'qt5compat qtimageformats'
|
||||
qt_arch: ""
|
||||
qt_version: "6.7.0"
|
||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||
|
||||
- os: macos-12
|
||||
name: macOS-Legacy
|
||||
@ -108,7 +108,7 @@ jobs:
|
||||
qt_ver: 5
|
||||
qt_host: mac
|
||||
qt_version: "5.15.2"
|
||||
qt_modules: ""
|
||||
qt_modules: "qtnetworkauth"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@ -150,6 +150,7 @@ jobs:
|
||||
quazip-qt6:p
|
||||
ccache:p
|
||||
qt6-5compat:p
|
||||
qt6-networkauth:p
|
||||
cmark:p
|
||||
|
||||
- name: Force newer ccache
|
||||
|
@ -282,7 +282,7 @@ endif()
|
||||
include(QtVersionlessBackport)
|
||||
if(Launcher_QT_VERSION_MAJOR EQUAL 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)
|
||||
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")
|
||||
elseif(Launcher_QT_VERSION_MAJOR EQUAL 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)
|
||||
|
||||
if(NOT Launcher_FORCE_BUNDLED_LIBS)
|
||||
@ -523,7 +523,6 @@ if(NOT cmark_FOUND)
|
||||
else()
|
||||
message(STATUS "Using system cmark")
|
||||
endif()
|
||||
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
|
||||
add_subdirectory(libraries/gamemode)
|
||||
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
|
||||
if (NOT ghc_filesystem_FOUND)
|
||||
|
26
COPYING.md
26
COPYING.md
@ -333,32 +333,6 @@
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
Copyright (c) 2017-2022, Feral Interactive
|
||||
|
@ -1234,7 +1234,6 @@ target_link_libraries(Launcher_logic
|
||||
tomlplusplus::tomlplusplus
|
||||
qdcss
|
||||
BuildConfig
|
||||
Katabasis
|
||||
Qt${QT_VERSION_MAJOR}::Widgets
|
||||
ghcFilesystem::ghc_filesystem
|
||||
)
|
||||
@ -1252,6 +1251,7 @@ target_link_libraries(Launcher_logic
|
||||
Qt${QT_VERSION_MAJOR}::Concurrent
|
||||
Qt${QT_VERSION_MAJOR}::Gui
|
||||
Qt${QT_VERSION_MAJOR}::Widgets
|
||||
Qt${QT_VERSION_MAJOR}::NetworkAuth
|
||||
${Launcher_QT_LIBS}
|
||||
)
|
||||
target_link_libraries(Launcher_logic
|
||||
@ -1322,7 +1322,6 @@ if(Launcher_BUILD_UPDATER)
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
${Launcher_QT_LIBS}
|
||||
cmark::cmark
|
||||
Katabasis
|
||||
)
|
||||
|
||||
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
|
||||
|
@ -34,12 +34,43 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <katabasis/Bits.h>
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#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 {
|
||||
QString id;
|
||||
QString url;
|
||||
|
@ -37,8 +37,6 @@ class AuthStep : public QObject {
|
||||
|
||||
signals:
|
||||
void finished(AccountTaskState resultingState, QString message);
|
||||
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
|
||||
void hideVerificationUriAndCode();
|
||||
|
||||
protected:
|
||||
AccountData* m_data;
|
||||
|
@ -21,6 +21,7 @@ void AuthFlow::succeed()
|
||||
void AuthFlow::executeTask()
|
||||
{
|
||||
if (m_currentStep) {
|
||||
emitFailed("No task");
|
||||
return;
|
||||
}
|
||||
changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
|
||||
@ -39,38 +40,10 @@ void AuthFlow::nextStep()
|
||||
qDebug() << "AuthFlow:" << m_currentStep->describe();
|
||||
m_steps.pop_front();
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (changeState(resultingState, message)) {
|
||||
@ -81,54 +54,61 @@ void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
|
||||
bool AuthFlow::changeState(AccountTaskState newState, QString reason)
|
||||
{
|
||||
m_taskState = newState;
|
||||
// FIXME: virtual method invoked in constructor.
|
||||
// We want that behavior, but maybe make it less weird?
|
||||
setStatus(getStateMessage());
|
||||
setDetails(reason);
|
||||
switch (newState) {
|
||||
case AccountTaskState::STATE_CREATED: {
|
||||
setStatus(tr("Waiting..."));
|
||||
m_data->errorString.clear();
|
||||
return true;
|
||||
}
|
||||
case AccountTaskState::STATE_WORKING: {
|
||||
setStatus(m_currentStep ? m_currentStep->describe() : tr("Working..."));
|
||||
m_data->accountState = AccountState::Working;
|
||||
return true;
|
||||
}
|
||||
case AccountTaskState::STATE_SUCCEEDED: {
|
||||
setStatus(tr("Authentication task succeeded."));
|
||||
m_data->accountState = AccountState::Online;
|
||||
emitSucceeded();
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_OFFLINE: {
|
||||
setStatus(tr("Failed to contact the authentication server."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Offline;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_DISABLED: {
|
||||
setStatus(tr("Client ID has changed. New session needs to be created."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Disabled;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_SOFT: {
|
||||
setStatus(tr("Encountered an error during authentication."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Errored;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_HARD: {
|
||||
setStatus(tr("Failed to authenticate. The session has expired."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Expired;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_GONE: {
|
||||
setStatus(tr("Failed to authenticate. The account no longer exists."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Gone;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
setStatus(tr("..."));
|
||||
QString error = tr("Unknown account task state: %1").arg(int(newState));
|
||||
m_data->accountState = AccountState::Errored;
|
||||
emitFailed(error);
|
||||
|
@ -7,8 +7,6 @@
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
|
||||
#include <katabasis/DeviceFlow.h>
|
||||
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "tasks/Task.h"
|
||||
@ -27,18 +25,9 @@ class AuthFlow : public Task {
|
||||
AccountTaskState taskState() { return m_taskState; }
|
||||
|
||||
signals:
|
||||
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
|
||||
void hideVerificationUriAndCode();
|
||||
|
||||
void activityChanged(Katabasis::Activity activity);
|
||||
|
||||
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 nextStep();
|
||||
|
||||
|
@ -35,28 +35,44 @@
|
||||
|
||||
#include "MSAStep.h"
|
||||
|
||||
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
|
||||
#include <QAbstractOAuth2>
|
||||
#include <QDesktopServices>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#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)
|
||||
{
|
||||
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
|
||||
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
|
||||
auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this);
|
||||
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(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode);
|
||||
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
|
||||
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()
|
||||
@ -69,68 +85,22 @@ void MSAStep::perform()
|
||||
switch (m_action) {
|
||||
case Refresh: {
|
||||
if (m_data->msaClientID != m_clientId) {
|
||||
emit hideVerificationUriAndCode();
|
||||
emit finished(AccountTaskState::STATE_DISABLED,
|
||||
tr("Microsoft user authentication failed - client identification has changed."));
|
||||
}
|
||||
m_oauth2->refresh();
|
||||
oauth2.setRefreshToken(m_data->msaToken.refresh_token);
|
||||
oauth2.refreshAccessToken();
|
||||
return;
|
||||
}
|
||||
case Login: {
|
||||
QVariantMap extraOpts;
|
||||
extraOpts["prompt"] = "select_account";
|
||||
m_oauth2->setExtraRequestParams(extraOpts);
|
||||
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) {
|
||||
map->insert("prompt", "select_account");
|
||||
map->insert("cobrandid", "8058f65d-ce06-4c30-9559-473c9275a65d");
|
||||
});
|
||||
|
||||
*m_data = AccountData();
|
||||
m_data->msaClientID = m_clientId;
|
||||
m_oauth2->login();
|
||||
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."));
|
||||
oauth2.grant();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -36,11 +36,9 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
|
||||
#include <katabasis/DeviceFlow.h>
|
||||
|
||||
#include <QtNetworkAuth/qoauth2authorizationcodeflow.h>
|
||||
class MSAStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@ -54,11 +52,8 @@ class MSAStep : public AuthStep {
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||
|
||||
private:
|
||||
Katabasis::DeviceFlow* m_oauth2 = nullptr;
|
||||
Action m_action;
|
||||
QString m_clientId;
|
||||
QOAuth2AuthorizationCodeFlow oauth2;
|
||||
};
|
||||
|
@ -171,9 +171,9 @@ void NetRequest::downloadError(QNetworkReply::NetworkError error)
|
||||
}
|
||||
}
|
||||
// error happened during download.
|
||||
qCCritical(logCat) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error;
|
||||
qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with reason" << error;
|
||||
if (m_reply)
|
||||
qCCritical(logCat) << getUid().toString() << "HTTP Status " << replyStatusCode() << ";error " << errorString();
|
||||
qCCritical(logCat) << getUid().toString() << "HTTP Status" << replyStatusCode() << ";error" << errorString();
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
qt.*.debug=false
|
||||
# don't log credentials by default
|
||||
launcher.auth.credentials.debug=false
|
||||
katabasis.*.debug=false
|
||||
# remove the debug lines, other log levels still get through
|
||||
launcher.task.net.download.debug=false
|
||||
# enable or disable whole catageries
|
||||
|
@ -67,9 +67,6 @@ int MSALoginDialog::exec()
|
||||
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
|
||||
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
|
||||
connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
|
||||
connect(m_loginTask.get(), &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();
|
||||
|
||||
return QDialog::exec();
|
||||
@ -80,55 +77,6 @@ MSALoginDialog::~MSALoginDialog()
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void MSALoginDialog::externalLoginTick()
|
||||
{
|
||||
m_externalLoginElapsed++;
|
||||
ui->progressBar->setValue(m_externalLoginElapsed);
|
||||
ui->progressBar->repaint();
|
||||
|
||||
if (m_externalLoginElapsed >= m_externalLoginTimeout) {
|
||||
m_externalLoginTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn)
|
||||
{
|
||||
m_externalLoginElapsed = 0;
|
||||
m_externalLoginTimeout = expiresIn;
|
||||
|
||||
m_externalLoginTimer.setInterval(1000);
|
||||
m_externalLoginTimer.setSingleShot(false);
|
||||
m_externalLoginTimer.start();
|
||||
|
||||
ui->progressBar->setMaximum(expiresIn);
|
||||
ui->progressBar->setValue(m_externalLoginElapsed);
|
||||
|
||||
QString urlString = uri.toString();
|
||||
QString linkString = QString("<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)
|
||||
{
|
||||
ui->buttonBox->setEnabled(enable);
|
||||
|
@ -15,7 +15,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QTimer>
|
||||
#include <QtCore/QEventLoop>
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
@ -45,16 +44,9 @@ class MSALoginDialog : public QDialog {
|
||||
void onTaskSucceeded();
|
||||
void onTaskStatus(const QString& status);
|
||||
void onTaskProgress(qint64 current, qint64 total);
|
||||
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
|
||||
void hideVerificationUriAndCode();
|
||||
|
||||
void externalLoginTick();
|
||||
|
||||
private:
|
||||
Ui::MSALoginDialog* ui;
|
||||
MinecraftAccountPtr m_account;
|
||||
shared_qobject_ptr<AuthFlow> m_loginTask;
|
||||
QTimer m_externalLoginTimer;
|
||||
int m_externalLoginElapsed = 0;
|
||||
int m_externalLoginTimeout = 0;
|
||||
};
|
||||
|
@ -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.
|
||||
|
||||
## Katabasis
|
||||
|
||||
Oauth2 library customized for Microsoft authentication.
|
||||
|
||||
This is a fork of the [O2 library](https://github.com/pipacs/o2).
|
||||
|
||||
MIT licensed.
|
||||
|
||||
## launcher
|
||||
|
||||
Java launcher part for Minecraft.
|
||||
|
2
libraries/katabasis/.gitignore
vendored
2
libraries/katabasis/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
build/
|
||||
*.kdev4
|
@ -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)
|
@ -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.
|
@ -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)
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue
Block a user