1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#ifndef QT_NO_HTTP
5
6#include <qoauth2authorizationcodeflow.h>
7#include <private/qoauth2authorizationcodeflow_p.h>
8
9#include <qmap.h>
10#include <qurl.h>
11#include <qvariant.h>
12#include <qurlquery.h>
13#include <qjsonobject.h>
14#include <qjsondocument.h>
15#include <qauthenticator.h>
16#include <qoauthhttpserverreplyhandler.h>
17
18#include <functional>
19
20QT_BEGIN_NAMESPACE
21
22using namespace Qt::StringLiterals;
23
24/*!
25 \class QOAuth2AuthorizationCodeFlow
26 \inmodule QtNetworkAuth
27 \ingroup oauth
28 \brief The QOAuth2AuthorizationCodeFlow class provides an
29 implementation of the
30 \l {https://tools.ietf.org/html/rfc6749#section-4.1}
31 {Authorization Code Grant} flow.
32 \since 5.8
33
34 This class implements the
35 \l {https://tools.ietf.org/html/rfc6749#section-4.1}
36 {Authorization Code Grant} flow, which is used both to obtain and
37 to refresh access tokens. It is a redirection-based flow so the
38 user will need access to a web browser.
39*/
40
41/*!
42 \property QOAuth2AuthorizationCodeFlow::accessTokenUrl
43 \brief This property holds the URL used to convert the temporary
44 code received during the authorization response.
45
46 \b {See also}:
47 \l {https://tools.ietf.org/html/rfc6749#section-4.1.3}{Access
48 Token Request}
49*/
50
51QOAuth2AuthorizationCodeFlowPrivate::QOAuth2AuthorizationCodeFlowPrivate(
52 const QUrl &authorizationUrl, const QUrl &accessTokenUrl, const QString &clientIdentifier,
53 QNetworkAccessManager *manager) :
54 QAbstractOAuth2Private(qMakePair(value1: clientIdentifier, value2: QString()), authorizationUrl, manager),
55 accessTokenUrl(accessTokenUrl)
56{
57 responseType = QStringLiteral("code");
58}
59
60void QOAuth2AuthorizationCodeFlowPrivate::_q_handleCallback(const QVariantMap &data)
61{
62 Q_Q(QOAuth2AuthorizationCodeFlow);
63 using Key = QAbstractOAuth2Private::OAuth2KeyString;
64
65 if (status != QAbstractOAuth::Status::NotAuthenticated) {
66 qCWarning(loggingCategory) << "Authorization stage: callback in unexpected status:"
67 << static_cast<int>(status) << ", ignoring the callback";
68 return;
69 }
70
71 Q_ASSERT(!state.isEmpty());
72
73 const QString error = data.value(key: Key::error).toString();
74 const QString code = data.value(key: Key::code).toString();
75 const QString receivedState = data.value(key: Key::state).toString();
76 if (error.size()) {
77 // RFC 6749, Section 5.2 Error Response
78 const QString uri = data.value(key: Key::errorUri).toString();
79 const QString description = data.value(key: Key::errorDescription).toString();
80 qCWarning(loggingCategory, "Authorization stage: AuthenticationError: %s(%s): %s",
81 qPrintable(error), qPrintable(uri), qPrintable(description));
82 Q_EMIT q->error(error, errorDescription: description, uri);
83 // Emit also requestFailed() so that it is a signal for all errors
84 emit q->requestFailed(error: QAbstractOAuth::Error::ServerError);
85 return;
86 }
87
88 if (code.isEmpty()) {
89 qCWarning(loggingCategory, "Authorization stage: Code not received");
90 emit q->requestFailed(error: QAbstractOAuth::Error::OAuthTokenNotFoundError);
91 return;
92 }
93 if (receivedState.isEmpty()) {
94 qCWarning(loggingCategory, "Authorization stage: State not received");
95 emit q->requestFailed(error: QAbstractOAuth::Error::ServerError);
96 return;
97 }
98 if (state != receivedState) {
99 qCWarning(loggingCategory) << "Authorization stage: State mismatch";
100 emit q->requestFailed(error: QAbstractOAuth::Error::ServerError);
101 return;
102 }
103
104 setStatus(QAbstractOAuth::Status::TemporaryCredentialsReceived);
105
106 QVariantMap copy(data);
107 copy.remove(key: Key::code);
108 extraTokens = copy;
109 q->requestAccessToken(code);
110}
111
112void QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFinished(const QVariantMap &values)
113{
114 Q_Q(QOAuth2AuthorizationCodeFlow);
115 using Key = QAbstractOAuth2Private::OAuth2KeyString;
116
117 if (values.contains(key: Key::error)) {
118 _q_accessTokenRequestFailed(error: QAbstractOAuth::Error::ServerError,
119 errorString: values.value(key: Key::error).toString());
120 return;
121 }
122
123 bool ok;
124 const QString accessToken = values.value(key: Key::accessToken).toString();
125 tokenType = values.value(key: Key::tokenType).toString();
126 int expiresIn = values.value(key: Key::expiresIn).toInt(ok: &ok);
127 if (!ok)
128 expiresIn = -1;
129 if (values.value(key: Key::refreshToken).isValid())
130 q->setRefreshToken(values.value(key: Key::refreshToken).toString());
131 scope = values.value(key: Key::scope).toString();
132 if (accessToken.isEmpty()) {
133 _q_accessTokenRequestFailed(error: QAbstractOAuth::Error::OAuthTokenNotFoundError,
134 errorString: "Access token not received"_L1);
135 return;
136 }
137 q->setToken(accessToken);
138
139 const QDateTime currentDateTime = QDateTime::currentDateTime();
140 if (expiresIn > 0 && currentDateTime.secsTo(expiresAt) != expiresIn) {
141 expiresAt = currentDateTime.addSecs(secs: expiresIn);
142 Q_EMIT q->expirationAtChanged(expiration: expiresAt);
143 }
144
145 QVariantMap copy(values);
146 copy.remove(key: Key::accessToken);
147 copy.remove(key: Key::expiresIn);
148 copy.remove(key: Key::refreshToken);
149 copy.remove(key: Key::scope);
150 copy.remove(key: Key::tokenType);
151 extraTokens.insert(map: copy);
152
153 setStatus(QAbstractOAuth::Status::Granted);
154}
155
156void QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFailed(QAbstractOAuth::Error error,
157 const QString& errorString)
158{
159 Q_Q(QOAuth2AuthorizationCodeFlow);
160 qCWarning(loggingCategory) << "Token request failed:" << errorString;
161 // If we were refreshing, reset status to Granted if we have an access token.
162 // The access token might still be valid, and even if it wouldn't be,
163 // refreshing can be attempted again.
164 if (q->status() == QAbstractOAuth::Status::RefreshingToken) {
165 if (!q->token().isEmpty())
166 setStatus(QAbstractOAuth::Status::Granted);
167 else
168 setStatus(QAbstractOAuth::Status::NotAuthenticated);
169 }
170 emit q->requestFailed(error);
171}
172
173void QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate(QNetworkReply *reply,
174 QAuthenticator *authenticator)
175{
176 if (reply == currentReply){
177 const auto url = reply->url();
178 if (url == accessTokenUrl) {
179 authenticator->setUser(clientIdentifier);
180 authenticator->setPassword(QString());
181 }
182 }
183}
184
185/*!
186 Constructs a QOAuth2AuthorizationCodeFlow object with parent
187 object \a parent.
188*/
189QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QObject *parent) :
190 QOAuth2AuthorizationCodeFlow(nullptr,
191 parent)
192{}
193
194/*!
195 Constructs a QOAuth2AuthorizationCodeFlow object using \a parent
196 as parent and sets \a manager as the network access manager.
197*/
198QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QNetworkAccessManager *manager,
199 QObject *parent) :
200 QOAuth2AuthorizationCodeFlow(QString(),
201 manager,
202 parent)
203{}
204
205/*!
206 Constructs a QOAuth2AuthorizationCodeFlow object using \a parent
207 as parent and sets \a manager as the network access manager. The
208 client identifier is set to \a clientIdentifier.
209*/
210QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(const QString &clientIdentifier,
211 QNetworkAccessManager *manager,
212 QObject *parent) :
213 QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(QUrl(), QUrl(), clientIdentifier,
214 manager),
215 parent)
216{}
217
218/*!
219 Constructs a QOAuth2AuthorizationCodeFlow object using \a parent
220 as parent and sets \a manager as the network access manager. The
221 authenticate URL is set to \a authenticateUrl and the access
222 token URL is set to \a accessTokenUrl.
223*/
224QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(const QUrl &authenticateUrl,
225 const QUrl &accessTokenUrl,
226 QNetworkAccessManager *manager,
227 QObject *parent) :
228 QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(authenticateUrl, accessTokenUrl,
229 QString(), manager),
230 parent)
231{}
232
233/*!
234 Constructs a QOAuth2AuthorizationCodeFlow object using \a parent
235 as parent and sets \a manager as the network access manager. The
236 client identifier is set to \a clientIdentifier the authenticate
237 URL is set to \a authenticateUrl and the access token URL is set
238 to \a accessTokenUrl.
239*/
240QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(const QString &clientIdentifier,
241 const QUrl &authenticateUrl,
242 const QUrl &accessTokenUrl,
243 QNetworkAccessManager *manager,
244 QObject *parent) :
245 QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(authenticateUrl, accessTokenUrl,
246 clientIdentifier, manager),
247 parent)
248{}
249
250/*!
251 Destroys the QOAuth2AuthorizationCodeFlow instance.
252*/
253QOAuth2AuthorizationCodeFlow::~QOAuth2AuthorizationCodeFlow()
254{}
255
256/*!
257 Returns the URL used to request the access token.
258 \sa setAccessTokenUrl()
259*/
260QUrl QOAuth2AuthorizationCodeFlow::accessTokenUrl() const
261{
262 Q_D(const QOAuth2AuthorizationCodeFlow);
263 return d->accessTokenUrl;
264}
265
266/*!
267 Sets the URL used to request the access token to
268 \a accessTokenUrl.
269*/
270void QOAuth2AuthorizationCodeFlow::setAccessTokenUrl(const QUrl &accessTokenUrl)
271{
272 Q_D(QOAuth2AuthorizationCodeFlow);
273 if (d->accessTokenUrl != accessTokenUrl) {
274 d->accessTokenUrl = accessTokenUrl;
275 Q_EMIT accessTokenUrlChanged(accessTokenUrl);
276 }
277}
278
279/*!
280 Starts the authentication flow as described in
281 \l {https://tools.ietf.org/html/rfc6749#section-4.1}{The OAuth
282 2.0 Authorization Framework}
283*/
284void QOAuth2AuthorizationCodeFlow::grant()
285{
286 Q_D(QOAuth2AuthorizationCodeFlow);
287 if (d->authorizationUrl.isEmpty()) {
288 qCWarning(d->loggingCategory, "No authenticate Url set");
289 return;
290 }
291 if (d->accessTokenUrl.isEmpty()) {
292 qCWarning(d->loggingCategory, "No request access token Url set");
293 return;
294 }
295
296 resourceOwnerAuthorization(url: d->authorizationUrl);
297}
298
299/*!
300 Call this function to refresh the token. Access tokens are not
301 permanent. After a time specified along with the access token
302 when it was obtained, the access token will become invalid.
303
304 If refreshing the token fails and an access token exists, the status is
305 set to QAbstractOAuth::Status::Granted, and to
306 QAbstractOAuth::Status::NotAuthenticated otherwise.
307
308 \sa QAbstractOAuth::requestFailed()
309 \sa {https://tools.ietf.org/html/rfc6749#section-1.5}{Refresh
310 Token}
311*/
312void QOAuth2AuthorizationCodeFlow::refreshAccessToken()
313{
314 Q_D(QOAuth2AuthorizationCodeFlow);
315
316 if (d->refreshToken.isEmpty()) {
317 qCWarning(d->loggingCategory, "Cannot refresh access token. Empty refresh token");
318 return;
319 }
320 if (d->status == Status::RefreshingToken) {
321 qCWarning(d->loggingCategory, "Cannot refresh access token. "
322 "Refresh Access Token is already in progress");
323 return;
324 }
325
326 using Key = QAbstractOAuth2Private::OAuth2KeyString;
327
328 QMultiMap<QString, QVariant> parameters;
329 QNetworkRequest request(d->accessTokenUrl);
330#ifndef QT_NO_SSL
331 if (d->sslConfiguration && !d->sslConfiguration->isNull())
332 request.setSslConfiguration(*d->sslConfiguration);
333#endif
334 QUrlQuery query;
335 parameters.insert(key: Key::grantType, QStringLiteral("refresh_token"));
336 parameters.insert(key: Key::refreshToken, value: d->refreshToken);
337 parameters.insert(key: Key::redirectUri, value: QUrl::toPercentEncoding(callback()));
338 parameters.insert(key: Key::clientIdentifier, value: d->clientIdentifier);
339 parameters.insert(key: Key::clientSharedSecret, value: d->clientIdentifierSharedKey);
340 if (d->modifyParametersFunction)
341 d->modifyParametersFunction(Stage::RefreshingAccessToken, &parameters);
342 query = QAbstractOAuthPrivate::createQuery(parameters);
343 request.setHeader(header: QNetworkRequest::ContentTypeHeader,
344 QStringLiteral("application/x-www-form-urlencoded"));
345
346 const QString data = query.toString(encoding: QUrl::FullyEncoded);
347 d->currentReply = d->networkAccessManager()->post(request, data: data.toUtf8());
348 setStatus(Status::RefreshingToken);
349
350 QNetworkReply *reply = d->currentReply.data();
351 QAbstractOAuthReplyHandler *handler = replyHandler();
352 connect(sender: reply, signal: &QNetworkReply::finished,
353 slot: [handler, reply]() { handler->networkReplyFinished(reply); });
354 connect(sender: reply, signal: &QNetworkReply::finished, context: reply, slot: &QNetworkReply::deleteLater);
355 QObjectPrivate::connect(sender: d->replyHandler.data(), signal: &QAbstractOAuthReplyHandler::tokensReceived, receiverPrivate: d,
356 slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFinished,
357 type: Qt::UniqueConnection);
358 QObjectPrivate::connect(sender: d->networkAccessManager(),
359 signal: &QNetworkAccessManager::authenticationRequired,
360 receiverPrivate: d, slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate,
361 type: Qt::UniqueConnection);
362 QObjectPrivate::connect(sender: d->replyHandler.data(),
363 signal: &QAbstractOAuthReplyHandler::tokenRequestErrorOccurred,
364 receiverPrivate: d, slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFailed,
365 type: Qt::UniqueConnection);
366}
367
368/*!
369 Generates an authentication URL to be used in the
370 \l {https://tools.ietf.org/html/rfc6749#section-4.1.1}
371 {Authorization Request} using \a parameters.
372*/
373QUrl QOAuth2AuthorizationCodeFlow::buildAuthenticateUrl(const QMultiMap<QString, QVariant> &parameters)
374{
375 Q_D(QOAuth2AuthorizationCodeFlow);
376 using Key = QAbstractOAuth2Private::OAuth2KeyString;
377
378 if (d->state.isEmpty())
379 setState(QAbstractOAuth2Private::generateRandomState());
380 Q_ASSERT(!d->state.isEmpty());
381 const QString state = d->state;
382
383 QMultiMap<QString, QVariant> p(parameters);
384 QUrl url(d->authorizationUrl);
385 p.insert(key: Key::responseType, value: responseType());
386 p.insert(key: Key::clientIdentifier, value: d->clientIdentifier);
387 p.insert(key: Key::redirectUri, value: callback());
388 p.insert(key: Key::scope, value: d->scope);
389 p.insert(key: Key::state, value: state);
390 if (d->modifyParametersFunction)
391 d->modifyParametersFunction(Stage::RequestingAuthorization, &p);
392 url.setQuery(d->createQuery(parameters: p));
393 connect(sender: d->replyHandler.data(), signal: &QAbstractOAuthReplyHandler::callbackReceived, context: this,
394 slot: &QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived, type: Qt::UniqueConnection);
395 setStatus(QAbstractOAuth::Status::NotAuthenticated);
396 qCDebug(d->loggingCategory, "Generated URL: %s", qPrintable(url.toString()));
397 return url;
398}
399
400/*!
401 Requests an access token from the received \a code. The \a code
402 is received as a response when the user completes a successful
403 authentication in the browser.
404*/
405void QOAuth2AuthorizationCodeFlow::requestAccessToken(const QString &code)
406{
407 Q_D(QOAuth2AuthorizationCodeFlow);
408 using Key = QAbstractOAuth2Private::OAuth2KeyString;
409
410 QMultiMap<QString, QVariant> parameters;
411 QNetworkRequest request(d->accessTokenUrl);
412#ifndef QT_NO_SSL
413 if (d->sslConfiguration && !d->sslConfiguration->isNull())
414 request.setSslConfiguration(*d->sslConfiguration);
415#endif
416 QUrlQuery query;
417 parameters.insert(key: Key::grantType, QStringLiteral("authorization_code"));
418 parameters.insert(key: Key::code, value: QUrl::toPercentEncoding(code));
419 parameters.insert(key: Key::redirectUri, value: QUrl::toPercentEncoding(callback()));
420 parameters.insert(key: Key::clientIdentifier, value: QUrl::toPercentEncoding(d->clientIdentifier));
421 if (!d->clientIdentifierSharedKey.isEmpty())
422 parameters.insert(key: Key::clientSharedSecret, value: d->clientIdentifierSharedKey);
423 if (d->modifyParametersFunction)
424 d->modifyParametersFunction(Stage::RequestingAccessToken, &parameters);
425 query = QAbstractOAuthPrivate::createQuery(parameters);
426 request.setHeader(header: QNetworkRequest::ContentTypeHeader,
427 QStringLiteral("application/x-www-form-urlencoded"));
428
429 const QString data = query.toString(encoding: QUrl::FullyEncoded);
430 QNetworkReply *reply = d->networkAccessManager()->post(request, data: data.toUtf8());
431 d->currentReply = reply;
432 QAbstractOAuthReplyHandler *handler = replyHandler();
433 QObject::connect(sender: reply, signal: &QNetworkReply::finished,
434 slot: [handler, reply] { handler->networkReplyFinished(reply); });
435 connect(sender: reply, signal: &QNetworkReply::finished, context: reply, slot: &QNetworkReply::deleteLater);
436 QObjectPrivate::connect(sender: d->replyHandler.data(), signal: &QAbstractOAuthReplyHandler::tokensReceived, receiverPrivate: d,
437 slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFinished,
438 type: Qt::UniqueConnection);
439 QObjectPrivate::connect(sender: d->networkAccessManager(),
440 signal: &QNetworkAccessManager::authenticationRequired,
441 receiverPrivate: d, slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate,
442 type: Qt::UniqueConnection);
443 QObjectPrivate::connect(sender: d->replyHandler.data(),
444 signal: &QAbstractOAuthReplyHandler::tokenRequestErrorOccurred,
445 receiverPrivate: d, slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFailed,
446 type: Qt::UniqueConnection);
447}
448
449/*!
450 Builds an authentication URL using \a url and \a parameters. This
451 function emits an authorizeWithBrowser() signal to require user
452 interaction.
453*/
454void QOAuth2AuthorizationCodeFlow::resourceOwnerAuthorization(const QUrl &url,
455 const QMultiMap<QString, QVariant> &parameters)
456{
457 Q_D(QOAuth2AuthorizationCodeFlow);
458 if (Q_UNLIKELY(url != d->authorizationUrl)) {
459 qCWarning(d->loggingCategory, "Invalid URL: %s", qPrintable(url.toString()));
460 return;
461 }
462 const QUrl u = buildAuthenticateUrl(parameters);
463 QObjectPrivate::connect(sender: this, signal: &QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived, receiverPrivate: d,
464 slot: &QOAuth2AuthorizationCodeFlowPrivate::_q_handleCallback,
465 type: Qt::UniqueConnection);
466 Q_EMIT authorizeWithBrowser(url: u);
467}
468
469QT_END_NAMESPACE
470
471#include "moc_qoauth2authorizationcodeflow.cpp"
472
473#endif // QT_NO_HTTP
474

source code of qtnetworkauth/src/oauth/qoauth2authorizationcodeflow.cpp