diff options
Diffstat (limited to 'libraries/katabasis/include')
-rw-r--r-- | libraries/katabasis/include/katabasis/Bits.h | 33 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/Globals.h | 59 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/OAuth2.h | 233 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/PollServer.h | 48 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/Reply.h | 60 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/ReplyServer.h | 53 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/RequestParameter.h | 15 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/Requestor.h | 116 |
8 files changed, 617 insertions, 0 deletions
diff --git a/libraries/katabasis/include/katabasis/Bits.h b/libraries/katabasis/include/katabasis/Bits.h new file mode 100644 index 00000000..3fd2f530 --- /dev/null +++ b/libraries/katabasis/include/katabasis/Bits.h @@ -0,0 +1,33 @@ +#pragma once + +#include <QString> +#include <QDateTime> +#include <QMap> +#include <QVariantMap> + +namespace Katabasis { +enum class Activity { + Idle, + LoggingIn, + LoggingOut, + Refreshing +}; + +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; +}; + +} diff --git a/libraries/katabasis/include/katabasis/Globals.h b/libraries/katabasis/include/katabasis/Globals.h new file mode 100644 index 00000000..512745d3 --- /dev/null +++ b/libraries/katabasis/include/katabasis/Globals.h @@ -0,0 +1,59 @@ +#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"; + +} diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h new file mode 100644 index 00000000..4361691c --- /dev/null +++ b/libraries/katabasis/include/katabasis/OAuth2.h @@ -0,0 +1,233 @@ +#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); + + /// 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); + + /// 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/PollServer.h b/libraries/katabasis/include/katabasis/PollServer.h new file mode 100644 index 00000000..77103867 --- /dev/null +++ b/libraries/katabasis/include/katabasis/PollServer.h @@ -0,0 +1,48 @@ +#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; +}; + +} diff --git a/libraries/katabasis/include/katabasis/Reply.h b/libraries/katabasis/include/katabasis/Reply.h new file mode 100644 index 00000000..3af1d49f --- /dev/null +++ b/libraries/katabasis/include/katabasis/Reply.h @@ -0,0 +1,60 @@ +#pragma once + +#include <QList> +#include <QTimer> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QNetworkAccessManager> +#include <QByteArray> + +namespace Katabasis { + +/// 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); + +signals: + void error(QNetworkReply::NetworkError); + +public slots: + /// When time out occurs, the QNetworkReply's error() signal is triggered. + void onTimeOut(); + +public: + QNetworkReply *reply; +}; + +/// 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); + + /// 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_; +}; + +} diff --git a/libraries/katabasis/include/katabasis/ReplyServer.h b/libraries/katabasis/include/katabasis/ReplyServer.h new file mode 100644 index 00000000..bf47df69 --- /dev/null +++ b/libraries/katabasis/include/katabasis/ReplyServer.h @@ -0,0 +1,53 @@ +#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/include/katabasis/RequestParameter.h b/libraries/katabasis/include/katabasis/RequestParameter.h new file mode 100644 index 00000000..ca36934a --- /dev/null +++ b/libraries/katabasis/include/katabasis/RequestParameter.h @@ -0,0 +1,15 @@ +#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; +}; + +} diff --git a/libraries/katabasis/include/katabasis/Requestor.h b/libraries/katabasis/include/katabasis/Requestor.h new file mode 100644 index 00000000..61437f76 --- /dev/null +++ b/libraries/katabasis/include/katabasis/Requestor.h @@ -0,0 +1,116 @@ +#pragma once +#include <QObject> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QNetworkAccessManager> +#include <QUrl> +#include <QByteArray> +#include <QHttpMultiPart> + +#include "Reply.h" + +namespace Katabasis { + +class OAuth2; + +/// Makes authenticated requests. +class Requestor: public QObject { + Q_OBJECT + +public: + explicit Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent = 0); + ~Requestor(); + + + /// Some services require the access token to be sent as a Authentication HTTP header + /// and refuse requests with the access token in the query. + /// This function allows to use or ignore the access token in the query. + /// The default value of `true` means that the query will contain the access token. + /// By setting the value to false, the query will not contain the access token. + /// See: + /// https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-4.3 + /// https://tools.ietf.org/html/rfc6750#section-2.3 + + void setAddAccessTokenInQuery(bool value); + + /// Some services require the access token to be sent as a Authentication HTTP header. + /// This is the case for Twitch and Mixer. + /// When the access token expires and is refreshed, O2Requestor::retry() needs to update the Authentication HTTP header. + /// In order to do so, O2Requestor needs to know the format of the Authentication HTTP header. + void setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value); + +public slots: + /// Make a GET request. + /// @return Request ID or -1 if there are too many requests in the queue. + int get(const QNetworkRequest &req, int timeout = 60*1000); + + /// Make a POST request. + /// @return Request ID or -1 if there are too many requests in the queue. + int post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); + int post(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000); + + /// Make a PUT request. + /// @return Request ID or -1 if there are too many requests in the queue. + int put(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); + int put(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000); + + /// Make a HEAD request. + /// @return Request ID or -1 if there are too many requests in the queue. + int head(const QNetworkRequest &req, int timeout = 60*1000); + + /// Make a custom request. + /// @return Request ID or -1 if there are too many requests in the queue. + int customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout = 60*1000); + +signals: + + /// Emitted when a request has been completed or failed. + void finished(int id, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers); + + /// Emitted when an upload has progressed. + void uploadProgress(int id, qint64 bytesSent, qint64 bytesTotal); + +protected slots: + /// Handle refresh completion. + void onRefreshFinished(QNetworkReply::NetworkError error); + + /// Handle request finished. + void onRequestFinished(); + + /// Handle request error. + void onRequestError(QNetworkReply::NetworkError error); + + /// Re-try request (after successful token refresh). + void retry(); + + /// Finish the request, emit finished() signal. + void finish(); + + /// Handle upload progress. + void onUploadProgress(qint64 uploaded, qint64 total); + +protected: + int setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); + + enum Status { + Idle, Requesting, ReRequesting + }; + + QNetworkAccessManager *manager_; + OAuth2 *authenticator_; + QNetworkRequest request_; + QByteArray data_; + QHttpMultiPart* multipartData_; + QNetworkReply *reply_; + Status status_; + int id_; + QNetworkAccessManager::Operation operation_; + QUrl url_; + ReplyList timedReplies_; + QNetworkReply::NetworkError error_; + bool addAccessTokenInQuery_; + QString accessTokenInAuthenticationHTTPHeaderFormat_; + bool rawData_; +}; + +} |