diff options
| -rw-r--r-- | launcher/minecraft/auth/AccountList.cpp | 9 | ||||
| -rw-r--r-- | launcher/minecraft/auth/MinecraftAccount.cpp | 24 | ||||
| -rw-r--r-- | launcher/minecraft/auth/MinecraftAccount.h | 9 | ||||
| -rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.cpp | 109 | ||||
| -rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.h | 7 | ||||
| -rw-r--r-- | launcher/minecraft/auth/flows/MSAInteractive.cpp | 3 | ||||
| -rw-r--r-- | launcher/ui/widgets/VersionListView.cpp | 1 | ||||
| -rw-r--r-- | libraries/katabasis/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | libraries/katabasis/include/katabasis/Bits.h | 6 | ||||
| -rw-r--r-- | libraries/katabasis/include/katabasis/DeviceFlow.h | 150 | ||||
| -rw-r--r-- | libraries/katabasis/include/katabasis/OAuth2.h | 233 | ||||
| -rw-r--r-- | libraries/katabasis/include/katabasis/Reply.h | 7 | ||||
| -rw-r--r-- | libraries/katabasis/include/katabasis/ReplyServer.h | 53 | ||||
| -rw-r--r-- | libraries/katabasis/src/DeviceFlow.cpp | 450 | ||||
| -rw-r--r-- | libraries/katabasis/src/OAuth2.cpp | 672 | ||||
| -rw-r--r-- | libraries/katabasis/src/Reply.cpp | 17 | ||||
| -rwxr-xr-x | libraries/katabasis/src/ReplyServer.cpp | 182 |
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 ¶ms, 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> ¶meters); - - /// Set refresh token. - void setRefreshToken(const QString &v); - - /// Set token expiration time. - void setExpires(QDateTime v); - - /// Start polling authorization server - void startPollServer(const QVariantMap ¶ms, 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> ¶meters) { + QByteArray ret; + bool first = true; + for( auto & h: parameters) { + if (first) { + first = false; + } else { + ret.append("&"); < |
