aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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>
+#include <QUuid>
+#include <QDataStream>
+
+#include <QUrlQuery>
+
+#include "katabasis/DeviceFlow.h"
+#include "katabasis/PollServer.h"
+#include "katabasis/Globals.h"
+
+#include "JsonResponse.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("&");
<