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