1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 ////////////////////////////////////////////////////////////////////////////////
4 
5 //                                                                            //
6 // This file is part of QOAuth2.                                              //
7 // Copyright (c) 2014 Jacob Dawid <jacob@omg-it.works>                        //
8 //                                                                            //
9 // QOAuth2 is free software: you can redistribute it and/or modify            //
10 // it under the terms of the GNU Affero General Public License as             //
11 // published by the Free Software Foundation, either version 3 of the         //
12 // License, or (at your option) any later version.                            //
13 //                                                                            //
14 // QOAuth2 is distributed in the hope that it will be useful,                 //
15 // but WITHOUT ANY WARRANTY; without even the implied warranty of             //
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              //
17 // GNU Affero General Public License for more details.                        //
18 //                                                                            //
19 // You should have received a copy of the GNU Affero General Public           //
20 // License along with QOAuth2.                                                //
21 // If not, see <http://www.gnu.org/licenses/>.                                //
22 //                                                                            //
23 ////////////////////////////////////////////////////////////////////////////////
24 
25 #include "network-web/oauth2service.h"
26 
27 #include "definitions/definitions.h"
28 #include "gui/messagebox.h"
29 #include "miscellaneous/application.h"
30 #include "network-web/networkfactory.h"
31 #include "network-web/oauthhttphandler.h"
32 #include "network-web/webfactory.h"
33 
34 #include <QDebug>
35 #include <QInputDialog>
36 #include <QJsonDocument>
37 #include <QJsonObject>
38 #include <QNetworkReply>
39 #include <QNetworkRequest>
40 #include <QRandomGenerator>
41 
42 #include <cstdlib>
43 #include <utility>
44 
OAuth2Service(const QString & auth_url,const QString & token_url,const QString & client_id,const QString & client_secret,const QString & scope,QObject * parent)45 OAuth2Service::OAuth2Service(const QString& auth_url, const QString& token_url, const QString& client_id,
46                              const QString& client_secret, const QString& scope, QObject* parent)
47   : QObject(parent),
48   m_id(QString::number(QRandomGenerator::global()->generate())), m_timerId(-1),
49   m_redirectionHandler(new OAuthHttpHandler(tr("You can close this window now. Go back to %1.").arg(QSL(APP_NAME)), this)),
50   m_functorOnLogin(std::function<void()>()) {
51   m_tokenGrantType = QSL("authorization_code");
52   m_tokenUrl = QUrl(token_url);
53   m_authUrl = auth_url;
54 
55   m_clientId = client_id;
56   m_clientSecret = client_secret;
57   m_clientSecretId = m_clientSecretSecret = QString();
58   m_scope = scope;
59 
60   connect(&m_networkManager, &QNetworkAccessManager::finished, this, &OAuth2Service::tokenRequestFinished);
61   connect(m_redirectionHandler, &OAuthHttpHandler::authGranted, [this](const QString& auth_code, const QString& id) {
62     if (id.isEmpty() || id == m_id) {
63       // We process this further only if handler (static singleton) responded to our original request.
64       retrieveAccessToken(auth_code);
65     }
66   });
67   connect(m_redirectionHandler, &OAuthHttpHandler::authRejected, [this](const QString& error_description, const QString& id) {
68     Q_UNUSED(error_description)
69 
70     if (id.isEmpty() || id == m_id) {
71       // We process this further only if handler (static singleton) responded to our original request.
72       emit authFailed();
73     }
74   });
75 }
76 
~OAuth2Service()77 OAuth2Service::~OAuth2Service() {
78   qDebugNN << LOGSEC_OAUTH << "Destroying OAuth2Service instance.";
79 }
80 
bearer()81 QString OAuth2Service::bearer() {
82   if (!isFullyLoggedIn()) {
83     qApp->showGuiMessage(Notification::Event::LoginFailure,
84                          tr("You have to login first"),
85                          tr("Click here to login."),
86                          QSystemTrayIcon::MessageIcon::Critical,
87                          {}, {},
88                          tr("Login"),
89                          [this]() {
90       login();
91     });
92     return {};
93   }
94   else {
95     return QSL("Bearer %1").arg(accessToken());
96   }
97 }
98 
isFullyLoggedIn() const99 bool OAuth2Service::isFullyLoggedIn() const {
100   bool is_expiration_valid = tokensExpireIn() > QDateTime::currentDateTime();
101   bool do_tokens_exist = !refreshToken().isEmpty() && !accessToken().isEmpty();
102 
103   return is_expiration_valid && do_tokens_exist;
104 }
105 
setOAuthTokenGrantType(QString grant_type)106 void OAuth2Service::setOAuthTokenGrantType(QString grant_type) {
107   m_tokenGrantType = std::move(grant_type);
108 }
109 
oAuthTokenGrantType()110 QString OAuth2Service::oAuthTokenGrantType() {
111   return m_tokenGrantType;
112 }
113 
timerEvent(QTimerEvent * event)114 void OAuth2Service::timerEvent(QTimerEvent* event) {
115   if (m_timerId >= 0 && event->timerId() == m_timerId) {
116     event->accept();
117 
118     QDateTime window_about_expire = tokensExpireIn().addSecs(-60 * 15);
119 
120     if (window_about_expire < QDateTime::currentDateTime()) {
121       // We try to refresh access token, because it probably expires soon.
122       qDebugNN << LOGSEC_OAUTH << "Refreshing automatically access token.";
123       refreshAccessToken();
124     }
125     else {
126       qDebugNN << LOGSEC_OAUTH << "Access token is not expired yet.";
127     }
128   }
129 
130   QObject::timerEvent(event);
131 }
132 
clientSecretSecret() const133 QString OAuth2Service::clientSecretSecret() const {
134   return m_clientSecretSecret;
135 }
136 
setClientSecretSecret(const QString & client_secret_secret)137 void OAuth2Service::setClientSecretSecret(const QString& client_secret_secret) {
138   m_clientSecretSecret = client_secret_secret;
139 }
140 
clientSecretId() const141 QString OAuth2Service::clientSecretId() const {
142   return m_clientSecretId;
143 }
144 
setClientSecretId(const QString & client_secret_id)145 void OAuth2Service::setClientSecretId(const QString& client_secret_id) {
146   m_clientSecretId = client_secret_id;
147 }
148 
id() const149 QString OAuth2Service::id() const {
150   return m_id;
151 }
152 
setId(const QString & id)153 void OAuth2Service::setId(const QString& id) {
154   m_id = id;
155 }
156 
retrieveAccessToken(const QString & auth_code)157 void OAuth2Service::retrieveAccessToken(const QString& auth_code) {
158   QNetworkRequest networkRequest;
159 
160   networkRequest.setUrl(m_tokenUrl);
161   networkRequest.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
162 
163   QString content = QString("client_id=%1&"
164                             "client_secret=%2&"
165                             "code=%3&"
166                             "redirect_uri=%5&"
167                             "grant_type=%4").arg(properClientId(),
168                                                  properClientSecret(),
169                                                  auth_code,
170                                                  m_tokenGrantType,
171                                                  m_redirectionHandler->listenAddressPort());
172 
173   qDebugNN << LOGSEC_OAUTH << "Posting data for access token retrieval:" << QUOTE_W_SPACE_DOT(content);
174   m_networkManager.post(networkRequest, content.toUtf8());
175 }
176 
refreshAccessToken(const QString & refresh_token)177 void OAuth2Service::refreshAccessToken(const QString& refresh_token) {
178   auto real_refresh_token = refresh_token.isEmpty() ? refreshToken() : refresh_token;
179   QNetworkRequest networkRequest;
180 
181   networkRequest.setUrl(m_tokenUrl);
182   networkRequest.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
183 
184   QString content = QString("client_id=%1&"
185                             "client_secret=%2&"
186                             "refresh_token=%3&"
187                             "grant_type=%4").arg(properClientId(),
188                                                  properClientSecret(),
189                                                  real_refresh_token,
190                                                  QSL("refresh_token"));
191 
192   qApp->showGuiMessage(Notification::Event::LoginDataRefreshed,
193                        tr("Logging in via OAuth 2.0..."),
194                        tr("Refreshing login tokens for '%1'...").arg(m_tokenUrl.toString()),
195                        QSystemTrayIcon::MessageIcon::Information);
196 
197   qDebugNN << LOGSEC_OAUTH << "Posting data for access token refreshing:" << QUOTE_W_SPACE_DOT(content);
198   m_networkManager.post(networkRequest, content.toUtf8());
199 }
200 
tokenRequestFinished(QNetworkReply * network_reply)201 void OAuth2Service::tokenRequestFinished(QNetworkReply* network_reply) {
202   QByteArray repl = network_reply->readAll();
203   QJsonDocument json_document = QJsonDocument::fromJson(repl);
204   QJsonObject root_obj = json_document.object();
205 
206   qDebugNN << LOGSEC_OAUTH << "Token response:" << QUOTE_W_SPACE_DOT(QString::fromUtf8(json_document.toJson()));
207 
208   if (network_reply->error() != QNetworkReply::NetworkError::NoError) {
209     qWarningNN << LOGSEC_OAUTH
210                << "Network error when obtaining token response:"
211                << QUOTE_W_SPACE_DOT(network_reply->error());
212 
213     emit tokensRetrieveError(QString(), NetworkFactory::networkErrorText(network_reply->error()));
214   }
215   else if (root_obj.keys().contains(QSL("error"))) {
216     QString error = root_obj.value(QSL("error")).toString();
217     QString error_description = root_obj.value(QSL("error_description")).toString();
218 
219     qWarningNN << LOGSEC_OAUTH
220                << "JSON error when obtaining token response:"
221                << QUOTE_W_SPACE(error)
222                << QUOTE_W_SPACE_DOT(error_description);
223 
224     logout();
225 
226     emit tokensRetrieveError(error, error_description);
227   }
228   else {
229     int expires = root_obj.value(QL1S("expires_in")).toInt();
230 
231     setTokensExpireIn(QDateTime::currentDateTime().addSecs(expires));
232     setAccessToken(root_obj.value(QL1S("access_token")).toString());
233 
234     const QString refresh_token = root_obj.value(QL1S("refresh_token")).toString();
235 
236     if (!refresh_token.isEmpty()) {
237       setRefreshToken(refresh_token);
238     }
239 
240     qDebugNN << LOGSEC_OAUTH
241              << "Obtained refresh token" << QUOTE_W_SPACE(refreshToken())
242              << "- expires on date/time" << QUOTE_W_SPACE_DOT(tokensExpireIn());
243 
244     if (m_functorOnLogin != nullptr) {
245       m_functorOnLogin();
246     }
247 
248     emit tokensRetrieved(accessToken(), refreshToken(), expires);
249   }
250 
251   network_reply->deleteLater();
252 }
253 
properClientId() const254 QString OAuth2Service::properClientId() const {
255   return m_clientId.simplified().isEmpty()
256       ? m_clientSecretId
257       : m_clientId;
258 }
259 
properClientSecret() const260 QString OAuth2Service::properClientSecret() const {
261   return m_clientSecret.simplified().isEmpty()
262       ? m_clientSecretSecret
263       : m_clientSecret;
264 }
265 
accessToken() const266 QString OAuth2Service::accessToken() const {
267   return m_accessToken;
268 }
269 
setAccessToken(const QString & access_token)270 void OAuth2Service::setAccessToken(const QString& access_token) {
271   m_accessToken = access_token;
272 }
273 
tokensExpireIn() const274 QDateTime OAuth2Service::tokensExpireIn() const {
275   return m_tokensExpireIn;
276 }
277 
setTokensExpireIn(const QDateTime & tokens_expire_in)278 void OAuth2Service::setTokensExpireIn(const QDateTime& tokens_expire_in) {
279   m_tokensExpireIn = tokens_expire_in;
280 }
281 
clientSecret() const282 QString OAuth2Service::clientSecret() const {
283   return m_clientSecret;
284 }
285 
setClientSecret(const QString & client_secret)286 void OAuth2Service::setClientSecret(const QString& client_secret) {
287   m_clientSecret = client_secret;
288 }
289 
clientId() const290 QString OAuth2Service::clientId() const {
291   return m_clientId;
292 }
293 
setClientId(const QString & client_id)294 void OAuth2Service::setClientId(const QString& client_id) {
295   m_clientId = client_id;
296 }
297 
redirectUrl() const298 QString OAuth2Service::redirectUrl() const {
299   return m_redirectionHandler->listenAddressPort();
300 }
301 
setRedirectUrl(const QString & redirect_url,bool start_handler)302 void OAuth2Service::setRedirectUrl(const QString& redirect_url, bool start_handler) {
303   m_redirectionHandler->setListenAddressPort(redirect_url, start_handler);
304 }
305 
refreshToken() const306 QString OAuth2Service::refreshToken() const {
307   return m_refreshToken;
308 }
309 
setRefreshToken(const QString & refresh_token)310 void OAuth2Service::setRefreshToken(const QString& refresh_token) {
311   killRefreshTimer();
312   m_refreshToken = refresh_token;
313   startRefreshTimer();
314 }
315 
login(const std::function<void ()> & functor_when_logged_in)316 bool OAuth2Service::login(const std::function<void()>& functor_when_logged_in) {
317   m_functorOnLogin = functor_when_logged_in;
318 
319   if (!m_redirectionHandler->isListening()) {
320     qCriticalNN << LOGSEC_OAUTH
321                 << "Cannot log-in because OAuth redirection handler is not listening.";
322 
323     emit tokensRetrieveError(QString(), tr("Failed to start OAuth "
324                                            "redirection listener. Maybe your "
325                                            "rights are not high enough."));
326 
327     return false;
328   }
329 
330   bool did_token_expire = tokensExpireIn().isNull() || tokensExpireIn() < QDateTime::currentDateTime().addSecs(-120);
331   bool does_token_exist = !refreshToken().isEmpty();
332 
333   // We refresh current tokens only if:
334   //   1. We have some existing refresh token.
335   //   AND
336   //   2. We do not know its expiration date or it passed.
337   if (does_token_exist && did_token_expire) {
338     refreshAccessToken();
339     return false;
340   }
341   else if (!does_token_exist) {
342     retrieveAuthCode();
343     return false;
344   }
345   else {
346     functor_when_logged_in();
347     return true;
348   }
349 }
350 
logout(bool stop_redirection_handler)351 void OAuth2Service::logout(bool stop_redirection_handler) {
352   setTokensExpireIn(QDateTime());
353   setAccessToken(QString());
354   setRefreshToken(QString());
355 
356   qDebugNN << LOGSEC_OAUTH << "Clearing tokens.";
357 
358   if (stop_redirection_handler) {
359     m_redirectionHandler->stop();
360   }
361 }
362 
startRefreshTimer()363 void OAuth2Service::startRefreshTimer() {
364   if (!refreshToken().isEmpty()) {
365     m_timerId = startTimer(1000 * 60 * 15, Qt::TimerType::VeryCoarseTimer);
366   }
367 }
368 
killRefreshTimer()369 void OAuth2Service::killRefreshTimer() {
370   if (m_timerId > 0) {
371     killTimer(m_timerId);
372   }
373 }
374 
retrieveAuthCode()375 void OAuth2Service::retrieveAuthCode() {
376   QString auth_url = m_authUrl + QString("?client_id=%1&"
377                                          "scope=%2&"
378                                          "redirect_uri=%3&"
379                                          "response_type=code&"
380                                          "state=%4&"
381                                          "prompt=consent&"
382                                          "access_type=offline").arg(properClientId(),
383                                                                     m_scope,
384                                                                     m_redirectionHandler->listenAddressPort(),
385                                                                     m_id);
386 
387   // We run login URL in external browser, response is caught by light HTTP server.
388   qApp->web()->openUrlInExternalBrowser(auth_url);
389 }
390