diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ set(REQUIRED_QT_VERSION "5.9.0") -find_package(Qt5 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Core Gui Widgets Qml Quick WebSockets Network NetworkAuth Test) +find_package(Qt5 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Core Gui Widgets Keychain Qml Quick WebSockets Network NetworkAuth Test) find_package(KF5 ${KF5_VERSION} REQUIRED COMPONENTS Kirigami2 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,4 @@ - set (Ruqola_restapi_SRCS restapi/restapirequest.cpp restapi/restapiutil.cpp @@ -21,7 +20,6 @@ rocketchatbackend.cpp notification.cpp messagequeue.cpp - authentication.cpp rocketchatmessage.cpp typingnotification.cpp changetemporarystatus.cpp @@ -52,6 +50,9 @@ textconverter.cpp loadrecenthistorymanager.cpp unityservicemanager.cpp + + #OAuth + google.cpp ) ecm_qt_declare_logging_category(Ruqola_core_srcs HEADER ruqola_debug.h IDENTIFIER RUQOLA_LOG CATEGORY_NAME org.kde.ruqola) @@ -78,8 +79,12 @@ KF5::Notifications KF5::KIOCore KF5::SyntaxHighlighting + + #OAuth lib + /usr/local/lib/libo2.a ) + if (WIN32 OR APPLE) target_link_libraries(libruqolacore KF5::IconThemes) endif() diff --git a/src/authentication.cpp b/src/authentication.cpp deleted file mode 100644 --- a/src/authentication.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - - * Copyright 2016 Riccardo Iaconelli - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of version 3 of the license. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "ruqola.h" -#include "authentication.h" -#include "ddpapi/ddpclient.h" -#include "ruqola_debug.h" -#include "rocketchataccount.h" - -#include -#include - -Authentication::Authentication() -{ - getDataFromJson(); -} - -void Authentication::getDataFromJson() -{ - QFile f(QStringLiteral(":/client_secret.json")); - - QString val; - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - val = QString::fromLatin1(f.readAll()); - } else { - qCWarning(RUQOLA_LOG) << "Impossible to read client_secret.json"; - //TODO exit ? - } - - QJsonDocument document = QJsonDocument::fromJson(val.toUtf8()); - QJsonObject object = document.object(); - const auto settingsObject = object[QStringLiteral("web")].toObject(); - const QUrl authUri(settingsObject[QStringLiteral("auth_uri")].toString()); - const QUrl tokenUri(settingsObject[QStringLiteral("token_uri")].toString()); - const auto clientID = settingsObject[QStringLiteral("client_id")].toString(); - const auto clientSecret(settingsObject[QStringLiteral("client_secret")].toString()); - const auto redirectUrls = settingsObject[QStringLiteral("redirect_uris")].toArray(); - const QUrl redirectUrl(redirectUrls[0].toString()); - -/* - QString clientID = QString("143580046552-s4rmnq5mg008u76id0d3rl63od985hc6.apps.googleusercontent.com"); - QString clientSecret = QString("nyVm19iOjjtldcCZJ-7003xg"); - QString redirectUrl = QString("http://localhost:8080/cb/_oauth/google?close"); -*/ - - QSettings s; - s.setValue(QStringLiteral("clientID"), clientID); - m_clientID = clientID; - s.setValue(QStringLiteral("clientSecret"), clientSecret); - m_clientSecret = clientSecret; - s.setValue(QStringLiteral("redirectUrl"), redirectUrl); -} - -void Authentication::OAuthLogin() -{ - QJsonObject auth; - QJsonObject authKeys; - authKeys[QStringLiteral("credentialToken")] = m_clientID; - authKeys[QStringLiteral("credentialSecret")] = m_clientSecret; - - auth[QStringLiteral("oauth")] = authKeys; - qCDebug(RUQOLA_LOG) << "-------------------------"; - qCDebug(RUQOLA_LOG) << "-------------------------"; - qCDebug(RUQOLA_LOG) << "OAuth Json" << auth; - Ruqola::self()->rocketChatAccount()->ddp()->method(QStringLiteral("login"), QJsonDocument(auth)); - - QJsonArray requestPermissions; - requestPermissions.append(QStringLiteral("email")); - - QUuid state; - state = state.createUuid(); - QSettings s; - s.setValue(QStringLiteral("stateRandomNumber"), state); - - QJsonObject loginUrlParameters; - loginUrlParameters[QStringLiteral("client_id")] = m_clientID; - loginUrlParameters[QStringLiteral("response_type")] = QStringLiteral("code"); - loginUrlParameters[QStringLiteral("scope")] = QStringLiteral("openID profile email"); - loginUrlParameters[QStringLiteral("state")] = state.toString(); - - QJsonObject json; - json[QStringLiteral("requestPermissions")] = requestPermissions; - json[QStringLiteral("requestOfflineToken")] = true; - json[QStringLiteral("loginUrlParameters")] = loginUrlParameters; - json[QStringLiteral("loginHint")] = s.value(QStringLiteral("username")).toString(); - json[QStringLiteral("loginStyle")] = QStringLiteral("redirect"); - json[QStringLiteral("redirectUrl")] = s.value(QStringLiteral("redirectUrl")).toString(); - -// qCDebug(RUQOLA_LOG) << "OAuth Json" << json; -// Ruqola::self()->ddp()->method("login", QJsonDocument(json)); -} diff --git a/src/client_secret.json b/src/client_secret.json --- a/src/client_secret.json +++ b/src/client_secret.json @@ -1,10 +1,10 @@ {"web": - {"client_id":"143580046552-s4rmnq5mg008u76id0d3rl63od985hc6.apps.googleusercontent.com", + {"client_id":"788579520714-51vdlc1d4o5a86i0l77lp75ujmadu2qg.apps.googleusercontent.com", "project_id":"ruqola-161705", "auth_uri":"https://accounts.google.com/o/oauth2/auth", "token_uri":"https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", - "client_secret":"nyVm19iOjjtldcCZJ-7003xg", + "client_secret":"rQ-k1MxaCvYDbRxrsBtTMPoE", "redirect_uris":["http://localhost:8080/cb/_oauth/google?close"] } } diff --git a/src/ddpapi/ddpclient.h b/src/ddpapi/ddpclient.h --- a/src/ddpapi/ddpclient.h +++ b/src/ddpapi/ddpclient.h @@ -54,8 +54,9 @@ enum LoginType { Password, - Google + GoogleOAuth }; + Q_ENUM(LoginType) explicit DDPClient(RocketChatAccount *account = nullptr, QObject *parent = nullptr); ~DDPClient(); @@ -92,6 +93,11 @@ void subscribe(const QString &collection, const QJsonArray ¶ms); /** + * @brief Calls method to log in the user with credentialToken and credentialSecret got from Google.cpp + */ + void loginGoogle(); + + /** * @brief Calls method to log in the user with valid username and password */ Q_INVOKABLE void login(); @@ -134,6 +140,11 @@ */ QString cachePath() const; + /** + *@brief used to set loginType to GoogleOAuth when user presses "Login with Google" button + * */ + Q_INVOKABLE void setLoginType(LoginType t); + quint64 leaveRoom(const QString &roomID); quint64 hideRoom(const QString &roomID); quint64 clearUnreadMessages(const QString &roomID); @@ -197,7 +208,7 @@ void setLoginStatus(LoginStatus l); LoginType loginType() const; - Q_INVOKABLE void setLoginType(LoginType t); + QString mUrl; AbstractWebSocket *mWebSocket = nullptr; diff --git a/src/ddpapi/ddpclient.cpp b/src/ddpapi/ddpclient.cpp --- a/src/ddpapi/ddpclient.cpp +++ b/src/ddpapi/ddpclient.cpp @@ -31,6 +31,7 @@ #include "messagequeue.h" #include "ruqolalogger.h" #include "rocketchatbackend.h" +#include "google.h" #include #include @@ -539,7 +540,12 @@ if (root.value(QLatin1String("error")).toObject().value(QLatin1String("error")).toInt() == 403) { qCDebug(RUQOLA_DDPAPI_LOG) << "Wrong password or token expired"; - login(); // Let's keep trying to log in + if (loginType() == DDPClient::LoginType::GoogleOAuth) { + loginGoogle(); // Let's keep trying to log in + } else { + login(); // Let's keep trying to log in + } + } else { const QString token = root.value(QLatin1String("result")).toObject().value(QLatin1String("token")).toString(); mRocketChatAccount->settings()->setAuthToken(token); @@ -556,7 +562,11 @@ setLoginStatus(DDPClient::LoggingIn); //Ruqola::self()->authentication()->OAuthLogin(); - login(); // Try to resume auth token login + if (loginType() == DDPClient::LoginType::GoogleOAuth) { + loginGoogle(); // Let's keep trying to log in + } else { + login(); // Let's keep trying to log in + } } else if (messageType == QLatin1String("error")) { qCDebug(RUQOLA_DDPAPI_LOG) << "ERROR!!" << message; } else if (messageType == QLatin1String("ping")) { @@ -584,6 +594,19 @@ return method(QStringLiteral("loadHistory"), QJsonDocument(params), process_backlog); } +void DDPClient::loginGoogle() +{ + Google *api = new Google(this); + api->doOAuth(O2::GrantFlowAuthorizationCode); + + //When this signal is emitted from google.cpp it means it has called the login 'method' + //by sending credentialToken and credentialSecret + connect(api, &Google::loginMethodCalled, [=] { + m_loginJob = api->oauthLoginJob; + }); + +} + void DDPClient::login() { if (!mRocketChatAccount->settings()->password().isEmpty()) { diff --git a/src/authentication.h b/src/google.h rename from src/authentication.h rename to src/google.h --- a/src/authentication.h +++ b/src/google.h @@ -1,6 +1,6 @@ /* - - * Copyright 2016 Riccardo Iaconelli + * Copyright 2016 Riccardo Iaconelli + * Copyright 2018 Veluri Mithun * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -20,38 +20,49 @@ * */ -#ifndef AUTHENTICATION_H -#define AUTHENTICATION_H +#ifndef Google_H +#define Google_H + +#include -#include +#include "o2/o2google.h" +#include "o2/o0baseauth.h" -class Authentication +class Google : public QObject { -public: - Authentication(); + Q_OBJECT - /** - * @brief Extract info from Google Json API - */ +public: + explicit Google(QObject *parent = nullptr); void getDataFromJson(); - /** - * @brief Call DDPClient's @method method with OAuth params - */ - void OAuthLogin(); + unsigned oauthLoginJob; +Q_SIGNALS: + void extraTokensReady(const QVariantMap &extraTokens); + void linkingFailed(); + void linkingSucceeded(); + void loginMethodCalled(); - /** - * @brief Make requests to Google on behalf of user using access token - */ - void sendApiRequest(); +public Q_SLOTS: + void doOAuth(O2::GrantFlow grantFlowType); + void validateToken(); -private slots: - void onGranted(); +private Q_SLOTS: + void onLinkedChanged(); + void onLinkingSucceeded(); + void onOpenBrowser(const QUrl &url); + void onCloseBrowser(); + void onFinished(); + void OAuthLoginMethodParameter(); private: - bool m_authGranted = false; + O2Google *p_o2Google = nullptr; QString m_clientID; QString m_clientSecret; + QString m_authUri; + QString m_tokenUri; + QString m_accessToken; + bool m_isValidToken = false; }; -#endif // AUTHENTICATION_H +#endif // Google_H diff --git a/src/google.cpp b/src/google.cpp new file mode 100644 --- /dev/null +++ b/src/google.cpp @@ -0,0 +1,211 @@ +/* + * Copyright 2016 Riccardo Iaconelli + * Copyright 2018 Veluri Mithun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "google.h" +#include "ruqola.h" +#include "ruqola_debug.h" +#include "ruqola_ddpapi_debug.h" +#include "ddpapi/ddpclient.h" +#include "rocketchataccount.h" +#include "o2/o0globals.h" +#include "o2/o0settingsstore.h" +#include "o2/o2google.h" + +Google::Google(QObject *parent) + :QObject(parent) { + p_o2Google = new O2Google(this); + + getDataFromJson(); + + p_o2Google->setClientId(m_clientID); + p_o2Google->setClientSecret(m_clientSecret); + p_o2Google->setLocalPort(8888); //it is from redirect url(http://127.0.0.1:8888/) + p_o2Google->setRequestUrl(m_authUri); // Use the desktop login UI + p_o2Google->setScope(QStringLiteral("email")); + + // Create a store object for writing the received tokens + O0SettingsStore *store = new O0SettingsStore(QLatin1String(O2_ENCRYPTION_KEY)); + store->setGroupKey(QStringLiteral("Google")); + p_o2Google->setStore(store); + + connect(p_o2Google, &O2Google::linkedChanged, this, &Google::onLinkedChanged); + connect(p_o2Google, &O2Google::linkingFailed, this, &Google::linkingFailed); + connect(p_o2Google, &O2Google::linkingSucceeded, this, &Google::onLinkingSucceeded); + connect(p_o2Google, &O2Google::openBrowser, this, &Google::onOpenBrowser); + connect(p_o2Google, &O2Google::closeBrowser, this, &Google::onCloseBrowser); + connect(p_o2Google, &O2Google::linkingSucceeded, this, &Google::OAuthLoginMethodParameter); +} + +void Google::getDataFromJson() +{ + QFile f(QStringLiteral(":/client_secret.json")); + + QString val; + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + val = QString::fromLatin1(f.readAll()); + } else { + qCWarning(RUQOLA_LOG) << "Impossible to read client_secret.json"; + //TODO exit ? + } + + //******github******* + //38a607244195a0d7af8 > clientID + //bb617841568d7c1e0c0888f292cf69b7b11d327e3 > clientSecret + //https://github.com/login/oauth/authorize + //https://github.com/login/oauth/access_token + QJsonDocument document = QJsonDocument::fromJson(val.toUtf8()); + QJsonObject object = document.object(); + const auto settingsObject = object[QStringLiteral("web")].toObject(); + const auto authUri(settingsObject[QStringLiteral("auth_uri")].toString()); + const auto clientID = settingsObject[QStringLiteral("client_id")].toString(); + const auto clientSecret(settingsObject[QStringLiteral("client_secret")].toString()); + + m_clientID = clientID; + m_clientSecret = clientSecret; + m_authUri = authUri; + m_tokenUri = QStringLiteral("https://accounts.google.com/o/oauth2/token"); +} + +void Google::doOAuth(O2::GrantFlow grantFlowType) +{ + qCDebug(RUQOLA_LOG) << QStringLiteral("Starting OAuth 2 with grant flow type:") << QStringLiteral("Authorization Grant Flow") + << QStringLiteral("..."); + p_o2Google->setGrantFlow(grantFlowType); + p_o2Google->unlink(); + + //TODO: refresh the token if it is expired(not valid) + validateToken(); + if (m_isValidToken) { + OAuthLoginMethodParameter(); + } else { + p_o2Google->link(); + } + + +} + +//currently not used +void Google::validateToken() +{ + if (!p_o2Google->linked()) { + qCWarning(RUQOLA_LOG) << "ERROR: Application is not linked!"; + Q_EMIT linkingFailed(); + return; + } + + QString accessToken = p_o2Google->token(); + QString debugUrlStr = QString(m_tokenUri).arg(accessToken); + QNetworkRequest request = QNetworkRequest(QUrl(debugUrlStr)); + QNetworkAccessManager *mgr = new QNetworkAccessManager(this); + QNetworkReply *reply = mgr->get(request); + connect(reply, &QNetworkReply::finished, this, &Google::onFinished); + qCDebug(RUQOLA_LOG) << QStringLiteral("Validating user token. Please wait..."); +} + +void Google::onOpenBrowser(const QUrl &url) +{ + QDesktopServices::openUrl(url); +} + +void Google::onCloseBrowser() +{ + //TODO: close the browser +} + +void Google::onLinkedChanged() +{ + qCDebug(RUQOLA_LOG) << QStringLiteral("Link changed!"); +} + +void Google::onLinkingSucceeded() +{ + O2Google *o1t = qobject_cast(sender()); + if (!o1t->linked()) { + return; + } + m_accessToken = o1t->token(); + QVariantMap extraTokens = o1t->extraTokens(); + if (!extraTokens.isEmpty()) { + Q_EMIT extraTokensReady(extraTokens); + qCDebug(RUQOLA_LOG) << QStringLiteral("Extra tokens in response:"); + foreach (const QString &key, extraTokens.keys()) { + qCDebug(RUQOLA_LOG) << key << QStringLiteral(":") + << (extraTokens.value(key).toString().left(3) + QStringLiteral("...")); + } + } +} + +//currently not used +void Google::onFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) { + qCWarning(RUQOLA_LOG) << QStringLiteral("NULL reply!"); + Q_EMIT linkingFailed(); + return; + } + + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qCWarning(RUQOLA_LOG) << QStringLiteral("Reply error:") << reply->error(); + qCWarning(RUQOLA_LOG) << QStringLiteral("Reason:") << reply->errorString(); + Q_EMIT linkingFailed(); + return; + } + + const QByteArray replyData = reply->readAll(); + bool valid = !replyData.contains("error"); + if (valid) { + qCDebug(RUQOLA_LOG) << QStringLiteral("Token is valid"); + Q_EMIT linkingSucceeded(); + m_isValidToken = true; + } else { + qCDebug(RUQOLA_LOG) << QStringLiteral("Token is invalid"); + Q_EMIT linkingFailed(); + } +} + +void Google::OAuthLoginMethodParameter() +{ + QJsonObject auth; + QJsonObject authKeys;// + authKeys[QStringLiteral("credentialToken")] = m_accessToken; + authKeys[QStringLiteral("credentialSecret")] = m_clientSecret; + + auth[QStringLiteral("oauth")] = authKeys; + qCDebug(RUQOLA_DDPAPI_LOG) << "-------------------------"; + qCDebug(RUQOLA_DDPAPI_LOG) << "-------------------------"; + qCDebug(RUQOLA_DDPAPI_LOG) << "OAuth Json" << auth; + + oauthLoginJob = Ruqola::self()->rocketChatAccount()->ddp()->method(QStringLiteral("login"), QJsonDocument(auth)); + Q_EMIT loginMethodCalled(); +} + diff --git a/src/qml/Login.qml b/src/qml/Login.qml --- a/src/qml/Login.qml +++ b/src/qml/Login.qml @@ -1,5 +1,4 @@ /* - * Copyright 2016 Riccardo Iaconelli * Copyright (c) 2017-2018 Montel Laurent * @@ -21,24 +20,23 @@ * */ - import QtQuick 2.9 import QtQuick.Controls 1.3 +import KDE.Ruqola.Ruqola 1.0 import org.kde.kirigami 2.1 as Kirigami import KDE.Ruqola.DDPClient 1.0 import KDE.Ruqola.RocketChatAccount 1.0 Kirigami.Page { id: loginForm property QtObject rcAccount + property QtObject rocketChatAccount: Ruqola.rocketChatAccount() property alias username: usernameField.text; property alias password: passField.text; property alias serverUrl: urlField.text; property alias accountName: nameField.text; - signal accepted() - - + signal accepted() implicitHeight: 400 implicitWidth: 300 @@ -137,10 +135,29 @@ enabled: (passField.text && urlField.text && usernameField.text) onClicked: loginForm.accepted() isDefault: true - } + } Item { id: spacer3 + width: 30 + height: 30 + } + + Button { + id: googleButton + + width: parent.width + text: i18n("Login with Google") + enabled: true + onClicked: { + loginForm.accepted() + rocketChatAccount.setLoginTypeToGoogleOAuth() + } + isDefault: true + } + + Item { + id: spacer4 width: 30 height: 30 diff --git a/src/rocketchataccount.h b/src/rocketchataccount.h --- a/src/rocketchataccount.h +++ b/src/rocketchataccount.h @@ -123,6 +123,7 @@ Q_INVOKABLE void downloadFile(const QString &downloadFileUrl, const QUrl &localFile); Q_INVOKABLE void starMessage(const QString &messageId, const QString &rid, bool starred); Q_INVOKABLE void uploadFile(const QString &description, const QUrl &fileUrl); + Q_INVOKABLE void setLoginTypeToGoogleOAuth(); void loadEmoji(const QJsonObject &obj); void parsePublicSettings(const QJsonObject &obj); diff --git a/src/rocketchataccount.cpp b/src/rocketchataccount.cpp --- a/src/rocketchataccount.cpp +++ b/src/rocketchataccount.cpp @@ -286,6 +286,11 @@ return mDdp; } +void RocketChatAccount::setLoginTypeToGoogleOAuth() +{ + ddp()->setLoginType(DDPClient::LoginType::GoogleOAuth); +} + DDPClient::LoginStatus RocketChatAccount::loginStatus() { if (mDdp) {