aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2021-11-28 18:42:01 +0100
committerPetr Mrázek <peterix@gmail.com>2021-11-28 18:42:01 +0100
commit285188ea532d0e4d1b2b798f1ab13e35202dca0d (patch)
tree60881ae834cb96eee61ab78e9845b58c8c6066db
parent0e31f77468a8bf391ef2b31124f7882b7d907881 (diff)
downloadPrismLauncher-285188ea532d0e4d1b2b798f1ab13e35202dca0d.tar.gz
PrismLauncher-285188ea532d0e4d1b2b798f1ab13e35202dca0d.tar.bz2
PrismLauncher-285188ea532d0e4d1b2b798f1ab13e35202dca0d.zip
GH-4071 handle network errors when logging in with MSA as 'soft'
This makes the tokens not expire when such errors happen. Only applies to MSA, not the XBox and Mojang steps afterwards. Further testing and improvements are still needed.
-rw-r--r--launcher/minecraft/auth/AccountList.cpp9
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp24
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.h9
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp109
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.h7
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.cpp3
-rw-r--r--launcher/ui/widgets/VersionListView.cpp1
-rw-r--r--libraries/katabasis/CMakeLists.txt6
-rw-r--r--libraries/katabasis/include/katabasis/Bits.h6
-rw-r--r--libraries/katabasis/include/katabasis/DeviceFlow.h150
-rw-r--r--libraries/katabasis/include/katabasis/OAuth2.h233
-rw-r--r--libraries/katabasis/include/katabasis/Reply.h7
-rw-r--r--libraries/katabasis/include/katabasis/ReplyServer.h53
-rw-r--r--libraries/katabasis/src/DeviceFlow.cpp450
-rw-r--r--libraries/katabasis/src/OAuth2.cpp672
-rw-r--r--libraries/katabasis/src/Reply.cpp17
-rwxr-xr-xlibraries/katabasis/src/ReplyServer.cpp182
17 files changed, 722 insertions, 1216 deletions
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index 97ba48f4..d7537345 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -244,8 +244,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
}
case StatusColumn: {
- auto isActive = account->isActive();
- return isActive ? "Working" : "Ready";
+ if(account->isActive()) {
+ return tr("Working", "Account status");
+ }
+ if(account->isExpired()) {
+ return tr("Expired", "Account status");
+ }
+ return tr("Ready", "Account status");
}
case ProfileNameColumn: {
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 5cfec49d..30ed6afe 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -34,6 +34,11 @@
#include "flows/MojangRefresh.h"
#include "flows/MojangLogin.h"
+MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
+ m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
+}
+
+
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
MinecraftAccountPtr account(new MinecraftAccount());
if(account->data.resumeStateFromV2(json)) {
@@ -52,7 +57,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
{
- MinecraftAccountPtr account(new MinecraftAccount());
+ MinecraftAccountPtr account = new MinecraftAccount();
account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
@@ -91,6 +96,23 @@ AccountStatus MinecraftAccount::accountStatus() const {
}
}
+bool MinecraftAccount::isExpired() const {
+ switch(data.type) {
+ case AccountType::Mojang: {
+ return data.accessToken().isEmpty();
+ }
+ break;
+ case AccountType::MSA: {
+ return data.msaToken.validity == Katabasis::Validity::None;
+ }
+ break;
+ default: {
+ return true;
+ }
+ }
+}
+
+
QPixmap MinecraftAccount::getFace() const {
QPixmap skinTexture;
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index 928d3742..459ef903 100644
--- a/launcher/minecraft/auth/MinecraftAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -72,7 +72,7 @@ public: /* construction */
explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete;
//! Default constructor
- explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {};
+ explicit MinecraftAccount(QObject *parent = 0);
static MinecraftAccountPtr createFromUsername(const QString &username);
@@ -97,6 +97,10 @@ public: /* manipulation */
shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session);
public: /* queries */
+ QString internalId() const {
+ return m_internalId;
+ }
+
QString accountDisplayString() const {
return data.accountDisplayString();
}
@@ -119,6 +123,8 @@ public: /* queries */
bool isActive() const;
+ bool isExpired() const;
+
bool canMigrate() const {
return data.canMigrateToMSA;
}
@@ -168,6 +174,7 @@ signals:
// TODO: better signalling for the various possible state changes - especially errors
protected: /* variables */
+ QString m_internalId;
AccountData data;
// current task we are executing here
diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp
index 34e2bea8..00957fd4 100644
--- a/launcher/minecraft/auth/flows/AuthContext.cpp
+++ b/launcher/minecraft/auth/flows/AuthContext.cpp
@@ -18,7 +18,7 @@
#include <Application.h>
-using OAuth2 = Katabasis::OAuth2;
+using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
AuthContext::AuthContext(AccountData * data, QObject *parent) :
@@ -50,21 +50,17 @@ void AuthContext::initMSA() {
return;
}
- Katabasis::OAuth2::Options opts;
+ OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = APPLICATION->msaClientId();
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
- opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
// FIXME: OAuth2 is not aware of our fancy shared pointers
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
- m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice);
- connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
- connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
- connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
+ connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
}
void AuthContext::initMojang() {
@@ -78,7 +74,7 @@ void AuthContext::initMojang() {
}
void AuthContext::onMojangSucceeded() {
- doEntitlements();
+ doMinecraftProfile();
}
@@ -89,50 +85,56 @@ void AuthContext::onMojangFailed() {
changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed."));
}
-/*
-bool AuthContext::signOut() {
- if(isBusy()) {
- return false;
- }
-
- start();
-
- beginActivity(Activity::LoggingOut);
- m_oauth2->unlink();
- m_account = AccountData();
- finishActivity();
- return true;
-}
-*/
-
-void AuthContext::onOAuthLinkingFailed() {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
-}
-
-void AuthContext::onOAuthLinkingSucceeded() {
- emit hideVerificationUriAndCode();
- auto *o2t = qobject_cast<OAuth2 *>(sender());
- if (!o2t->linked()) {
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
- return;
- }
- QVariantMap extraTokens = o2t->extraTokens();
-#ifndef NDEBUG
- if (!extraTokens.isEmpty()) {
- qDebug() << "Extra tokens in response:";
- foreach (QString key, extraTokens.keys()) {
- qDebug() << "\t" << key << ":" << extraTokens.value(key);
+void AuthContext::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();
+ if (!m_oauth2->linked()) {
+ finishActivity();
+ changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
+ return;
+ }
+ QVariantMap extraTokens = m_oauth2->extraTokens();
+#ifndef NDEBUG
+ if (!extraTokens.isEmpty()) {
+ qDebug() << "Extra tokens in response:";
+ foreach (QString key, extraTokens.keys()) {
+ qDebug() << "\t" << key << ":" << extraTokens.value(key);
+ }
+ }
#endif
- doUserAuth();
-}
+ doUserAuth();
+ return;
+ }
+ case Katabasis::Activity::FailedSoft: {
+ emit hideVerificationUriAndCode();
+ finishActivity();
+ changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error."));
+ return;
+ }
+ case Katabasis::Activity::FailedGone:
+ case Katabasis::Activity::FailedHard: {
+ emit hideVerificationUriAndCode();
+ finishActivity();
+ changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
+ return;
+ }
+ default: {
+ emit hideVerificationUriAndCode();
+ finishActivity();
+ changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
+ return;
+ }
-void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
- // respond to activity change here
+ }
}
void AuthContext::doUserAuth() {
@@ -226,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() {
void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
if(error == QNetworkReply::AuthenticationRequiredError) {
- QJsonParseError jsonError;
+ QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
@@ -543,6 +545,10 @@ void AuthContext::onMinecraftProfileDone(
#endif
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
+ if(m_data->type == AccountType::Mojang) {
+ m_data->minecraftEntitlement.canPlayMinecraft = false;
+ m_data->minecraftEntitlement.ownsMinecraft = false;
+ }
m_data->minecraftProfile = MinecraftProfile();
succeed();
return;
@@ -560,6 +566,9 @@ void AuthContext::onMinecraftProfileDone(
}
if(m_data->type == AccountType::Mojang) {
+ auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
+ m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
+ m_data->minecraftEntitlement.ownsMinecraft = validProfile;
doMigrationEligibilityCheck();
}
else {
diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h
index dcb91613..5e4e9edc 100644
--- a/launcher/minecraft/auth/flows/AuthContext.h
+++ b/launcher/minecraft/auth/flows/AuthContext.h
@@ -7,7 +7,7 @@
#include <QNetworkReply>
#include <QImage>
-#include <katabasis/OAuth2.h>
+#include <katabasis/DeviceFlow.h>
#include "Yggdrasil.h"
#include "../AccountData.h"
#include "../AccountTask.h"
@@ -35,9 +35,6 @@ signals:
private slots:
// OAuth-specific callbacks
- void onOAuthLinkingSucceeded();
- void onOAuthLinkingFailed();
-
void onOAuthActivityChanged(Katabasis::Activity activity);
// Yggdrasil specific callbacks
@@ -87,7 +84,7 @@ protected:
void clearTokens();
protected:
- Katabasis::OAuth2 *m_oauth2 = nullptr;
+ Katabasis::DeviceFlow *m_oauth2 = nullptr;
Yggdrasil *m_yggdrasil = nullptr;
int m_requestsDone = 0;
diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp
index 6c597cf7..525aaf88 100644
--- a/launcher/minecraft/auth/flows/MSAInteractive.cpp
+++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp
@@ -17,7 +17,6 @@ void MSAInteractive::executeTask() {
m_oauth2->setExtraRequestParams(extraOpts);
beginActivity(Katabasis::Activity::LoggingIn);
- m_oauth2->unlink();
*m_data = AccountData();
- m_oauth2->link();
+ m_oauth2->login();
}
diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp
index 8424fedd..aba0b1a1 100644
--- a/launcher/ui/widgets/VersionListView.cpp
+++ b/launcher/ui/widgets/VersionListView.cpp
@@ -19,7 +19,6 @@
#include <QDrag>
#include <QPainter>
#include "VersionListView.h"
-#include "Common.h"
VersionListView::VersionListView(QWidget *parent)
:QTreeView ( parent )
diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt
index 2f9cb66d..c6115881 100644
--- a/libraries/katabasis/CMakeLists.txt
+++ b/libraries/katabasis/CMakeLists.txt
@@ -27,20 +27,18 @@ set(CMAKE_C_STANDARD 11)
find_package(Qt5 COMPONENTS Core Network REQUIRED)
set( katabasis_PRIVATE
- src/OAuth2.cpp
+ src/DeviceFlow.cpp
src/JsonResponse.cpp
src/JsonResponse.h
src/PollServer.cpp
src/Reply.cpp
- src/ReplyServer.cpp
)
set( katabasis_PUBLIC
- include/katabasis/OAuth2.h
+ include/katabasis/DeviceFlow.h
include/katabasis/Globals.h
include/katabasis/PollServer.h
include/katabasis/Reply.h
- include/katabasis/ReplyServer.h
include/katabasis/RequestParameter.h
)
diff --git a/libraries/katabasis/include/katabasis/Bits.h b/libraries/katabasis/include/katabasis/Bits.h
index 3fd2f530..f11f25d2 100644
--- a/libraries/katabasis/include/katabasis/Bits.h
+++ b/libraries/katabasis/include/katabasis/Bits.h
@@ -10,7 +10,11 @@ enum class Activity {
Idle,
LoggingIn,
LoggingOut,
- Refreshing
+ 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 {
diff --git a/libraries/katabasis/include/katabasis/DeviceFlow.h b/libraries/katabasis/include/katabasis/DeviceFlow.h
new file mode 100644
index 00000000..b68c92e0
--- /dev/null
+++ b/libraries/katabasis/include/katabasis/DeviceFlow.h
@@ -0,0 +1,150 @@
+#pragma once
+
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QPair>
+
+#include "Reply.h"
+#include "RequestParameter.h"
+#include "Bits.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;
+};
+
+}
diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h
deleted file mode 100644
index 9dbe5c71..00000000
--- a/libraries/katabasis/include/katabasis/OAuth2.h
+++ /dev/null
@@ -1,233 +0,0 @@
-#pragma once
-
-#include <QNetworkAccessManager>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QPair>
-
-#include "Reply.h"
-#include "RequestParameter.h"
-#include "Bits.h"
-
-namespace Katabasis {
-
-class ReplyServer;
-class PollServer;
-
-
-/*
- * FIXME: this is not as simple as it should be. it squishes 4 different grant flows into one big ball of mud
- * This serves no practical purpose and simply makes the code less readable / maintainable.
- *
- * Therefore: Split this into the 4 different OAuth2 flows that people can use as authentication steps. Write tests/examples for all of them.
- */
-
-/// Simple OAuth2 authenticator.
-class OAuth2: public QObject
-{
- Q_OBJECT
-public:
- Q_ENUMS(GrantFlow)
-
-public:
-
- struct Options {
- QString userAgent = QStringLiteral("Katabasis/1.0");
- QString redirectionUrl = QStringLiteral("http://localhost:%1");
- QString responseType = QStringLiteral("code");
- QString scope;
- QString clientIdentifier;
- QString clientSecret;
- QUrl authorizationUrl;
- QUrl accessTokenUrl;
- QVector<quint16> listenerPorts = { 0 };
- };
-
- /// Authorization flow types.
- enum GrantFlow {
- GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
- GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2
- GrantFlowResourceOwnerPasswordCredentials,
- GrantFlowDevice ///< @see https://tools.ietf.org/html/rfc8628#section-1
- };
-
- /// Authorization flow.
- GrantFlow grantFlow();
- void setGrantFlow(GrantFlow value);
-
-public:
- /// Are we authenticated?
- bool linked();
-
- /// Authentication token.
- QString token();
-
- /// Provider-specific extra tokens, available after a successful authentication
- QVariantMap extraTokens();
-
- /// Page content on local host after successful oauth.
- /// Provide it in case you do not want to close the browser, but display something
- QByteArray replyContent() const;
- void setReplyContent(const QByteArray &value);
-
-public:
-
- // TODO: remove
- /// Resource owner username.
- /// instances with the same (username, password) share the same "linked" and "token" properties.
- QString username();
- void setUsername(const QString &value);
-
- // TODO: remove
- /// Resource owner password.
- /// instances with the same (username, password) share the same "linked" and "token" properties.
- QString password();
- void setPassword(const QString &value);
-
- // TODO: remove
- /// API key.
- QString apiKey();
- void setApiKey(const QString &value);
-
- // TODO: remove
- /// Allow ignoring SSL errors?
- /// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem
- bool ignoreSslErrors();
- void setIgnoreSslErrors(bool ignoreSslErrors);
-
- // 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 OAuth2(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0);
-
- /// Get refresh token.
- QString refreshToken();
-
- /// Get token expiration time
- QDateTime expires();
-
-public slots:
- /// Authenticate.
- virtual void link();
-
- /// De-authenticate.
- virtual void unlink();
-
- /// Refresh token.
- bool refresh();
-
- /// Handle situation where reply server has opted to close its connection
- void serverHasClosed(bool paramsfound = false);
-
-signals:
- /// Emitted when a token refresh has been completed or failed.
- void refreshFinished(QNetworkReply::NetworkError error);
-
- /// 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 authentication/deauthentication succeeded.
- void linkingSucceeded();
-
- /// Emitted when authentication/deauthentication failed.
- void linkingFailed();
-
- void activityChanged(Activity activity);
-
-public slots:
- /// Handle verification response.
- virtual void onVerificationReceived(QMap<QString, QString>);
-
-protected slots:
- /// Handle completion of a token request.
- virtual void onTokenReplyFinished();
-
- /// Handle failure of a token request.
- virtual void onTokenReplyError(QNetworkReply::NetworkError error);
-
- /// Handle completion of a refresh request.
- virtual void onRefreshFinished();
-
- /// Handle failure of a refresh request.
- virtual void onRefreshError(QNetworkReply::NetworkError error);
-
- /// Handle completion of a Device Authorization Request
- virtual void onDeviceAuthReplyFinished();
-
-protected:
- /// Build HTTP request body.
- QByteArray buildRequestBody(const QMap<QString, QString> &parameters);
-
- /// 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 reply server
- void setReplyServer(ReplyServer *server);
-
- ReplyServer * replyServer() const;
-
- /// Set local poll server
- void setPollServer(PollServer *server);
-
- PollServer * pollServer() const;
-
- void updateActivity(Activity activity);
-
-protected:
- QString username_;
- QString password_;
-
- Options options_;
-
- QVariantMap extraReqParams_;
- QString apiKey_;
- QNetworkAccessManager *manager_ = nullptr;
- ReplyList timedReplies_;
- GrantFlow grantFlow_;
- QString grantType_;
-
-protected:
- QString redirectUri_;
- Token &token_;
-
- // this should be part of the reply server impl
- QByteArray replyContent_;
-
-private:
- ReplyServer *replyServer_ = nullptr;
- PollServer *pollServer_ = nullptr;
- Activity activity_ = Activity::Idle;
-};
-
-}
diff --git a/libraries/katabasis/include/katabasis/Reply.h b/libraries/katabasis/include/katabasis/Reply.h
index 3af1d49f..415cf4ec 100644
--- a/libraries/katabasis/include/katabasis/Reply.h
+++ b/libraries/katabasis/include/katabasis/Reply.h
@@ -9,12 +9,14 @@
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 = 60 * 1000, QObject *parent = 0);
+ Reply(QNetworkReply *reply, int timeOut = defaultTimeout, QObject *parent = 0);
signals:
void error(QNetworkReply::NetworkError);
@@ -25,6 +27,7 @@ public slots:
public:
QNetworkReply *reply;
+ bool timedOut = false;
};
/// List of O2Replies.
@@ -37,7 +40,7 @@ public:
virtual ~ReplyList();
/// Create a new O2Reply from a QNetworkReply, and add it to this list.
- void add(QNetworkReply *reply);
+ void add(QNetworkReply *reply, int timeOut = defaultTimeout);
/// Add an O2Reply to the list, while taking ownership of it.
void add(Reply *reply);
diff --git a/libraries/katabasis/include/katabasis/ReplyServer.h b/libraries/katabasis/include/katabasis/ReplyServer.h
deleted file mode 100644
index bf47df69..00000000
--- a/libraries/katabasis/include/katabasis/ReplyServer.h
+++ /dev/null
@@ -1,53 +0,0 @@
-#pragma once
-
-#include <QTcpServer>
-#include <QMap>
-#include <QByteArray>
-#include <QString>
-
-namespace Katabasis {
-
-/// HTTP server to process authentication response.
-class ReplyServer: public QTcpServer {
- Q_OBJECT
-
-public:
- explicit ReplyServer(QObject *parent = 0);
-
- /// Page content on local host after successful oauth - in case you do not want to close the browser, but display something
- Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent)
- QByteArray replyContent();
- void setReplyContent(const QByteArray &value);
-
- /// Seconds to keep listening *after* first response for a callback with token content
- Q_PROPERTY(int timeout READ timeout WRITE setTimeout)
- int timeout();
- void setTimeout(int timeout);
-
- /// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.)
- Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries)
- int callbackTries();
- void setCallbackTries(int maxtries);
-
- QString uniqueState();
- void setUniqueState(const QString &state);
-
-signals:
- void verificationReceived(QMap<QString, QString>);
- void serverClosed(bool); // whether it has found parameters
-
-public slots:
- void onIncomingConnection();
- void onBytesReady();
- QMap<QString, QString> parseQueryParams(QByteArray *data);
- void closeServer(QTcpSocket *socket = 0, bool hasparameters = false);
-
-protected:
- QByteArray replyContent_;
- int timeout_;
- int maxtries_;
- int tries_;
- QString uniqueState_;
-};
-
-}
diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp
new file mode 100644
index 00000000..5efd5e7b
--- /dev/null
+++ b/libraries/katabasis/src/DeviceFlow.cpp
@@ -0,0 +1,450 @@
+#include <QList>
+#include <QPair>
+#include <QDebug>
+#include <QTcpServer>
+#include <QMap>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkAccessManager>
+#include <QDateTime>
+#include <QCryptographicHash>
+#include <QTimer>
+#include <QVariantMap>