1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:authorization-protocol
4
5#include <private/qoauth2deviceauthorizationflow_p.h>
6#include <QtNetworkAuth/qoauth2deviceauthorizationflow.h>
7#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
8
9#include <QtCore/qdatetime.h>
10#include <QtCore/qmap.h>
11#include <QtCore/qjsondocument.h>
12#include <QtCore/qjsonobject.h>
13#include <QtCore/qurl.h>
14#include <QtCore/qurlquery.h>
15#include <QtCore/qvariant.h>
16
17#include <QtNetwork/qrestaccessmanager.h>
18#include <QtNetwork/qrestreply.h>
19
20#include <functional>
21
22QT_BEGIN_NAMESPACE
23
24using namespace Qt::StringLiterals;
25using namespace std::chrono_literals;
26using Error = QAbstractOAuth::Error;
27using Status = QAbstractOAuth::Status;
28using Stage = QAbstractOAuth::Stage;
29
30/*!
31 \class QOAuth2DeviceAuthorizationFlow
32 \inmodule QtNetworkAuth
33 \ingroup oauth
34 \brief The QOAuth2DeviceAuthorizationFlow class provides an
35 implementation of the
36 \l {https://datatracker.ietf.org/doc/html/rfc8628}
37 {Device Authorization Grant} flow.
38 \since 6.9
39
40 This class implements the
41 \l {https://datatracker.ietf.org/doc/html/rfc8628}
42 {Device Authorization Grant} flow, which is used to obtain
43 and refresh access tokens and ID tokens, particularly on devices lacking
44 a user-agent or with limited input capabilities. These devices include
45 televisions, machine HMIs, appliances, and IoT devices.
46
47 Device flow can be used on any platform and operating system
48 that is capable of SSL/TLS requests. Unlike
49 \l {QOAuth2AuthorizationCodeFlow}, this flow is not based on
50 redirects, and therefore does not use a
51 \l {QAbstractOAuthReplyHandler}{reply handler}.
52
53 \section1 Device Flow Usage
54
55 The following snippets illustrate the typical usage. First, we set up
56 the flow similarly to \l {QOAuth2AuthorizationCodeFlow}:
57 \snippet src_oauth_replyhandlers.cpp deviceflow-setup
58
59 Then we connect to
60 \l {QOAuth2DeviceAuthorizationFlow::}{authorizeWithUserCode}
61 signal to handle the user authorization:
62 \snippet src_oauth_replyhandlers.cpp deviceflow-handle-authorizewithusercode
63 This part is crucial to the flow, and how you handle it depends on your
64 specific use case. One way or another, the user needs to complete the
65 authorization.
66
67 Device flow does not define how this authorization completion
68 is done, making it versatile for different use cases.
69 This can be achieved by displaying the verification URI and user code
70 to the user, who can then navigate to it on another device.
71 Alternatively, you could present a QR code for the user to
72 scan with their mobile device, send to a companion application,
73 email to the user, and so on.
74
75 While authorization is pending, \l {QOAuth2DeviceAuthorizationFlow} polls
76 the server at specific intervals (typically 5 seconds) until the user
77 accepts or rejects the authorization, upon which the server responds
78 accordingly and the flow concludes.
79
80 Errors can be detected as follows:
81 \snippet src_oauth_replyhandlers.cpp deviceflow-handle-errors
82 \l {QAbstractOAuth2::serverReportedErrorOccurred()} signal can
83 be used to get information on specific RFC-defined errors.
84 However, unlike \l {QAbstractOAuth::requestFailed()}, it doesn't
85 cover errors such as network errors or client configuration errors.
86
87 Flow completion is detected similarly as with
88 \l {QOAuth2AuthorizationCodeFlow}, for example:
89 \snippet src_oauth_replyhandlers.cpp deviceflow-handle-grant
90*/
91
92/*!
93 \property QOAuth2DeviceAuthorizationFlow::userCode
94
95 This property holds the
96 \l {https://datatracker.ietf.org/doc/html/rfc8628#section-3.2}{user_code}
97 received in authorization response. This code is used by the user to
98 complete the authorization.
99
100 \sa verificationUrl, completeVerificationUrl, {Device Flow Usage}
101*/
102
103/*!
104 \property QOAuth2DeviceAuthorizationFlow::verificationUrl
105
106 This property holds the URL where user should enter the user code to
107 complete authorization.
108
109 \sa userCode, completeVerificationUrl, {Device Flow Usage}
110*/
111
112/*!
113 \property QOAuth2DeviceAuthorizationFlow::completeVerificationUrl
114
115 This property holds an URL for user to complete the authorization.
116 The URL itself contains the \c {user_code} and thus avoids the
117 need for user to enter the code manually. Support for this
118 complete URL varies between authorization servers.
119
120 \sa verificationUrl, {Device Flow Usage}
121*/
122
123/*!
124 \property QOAuth2DeviceAuthorizationFlow::polling
125
126 This property holds whether or not the flow is actively polling
127 for tokens.
128
129 \sa startTokenPolling(), stopTokenPolling()
130*/
131
132/*!
133 \property QOAuth2DeviceAuthorizationFlow::userCodeExpirationAt
134
135 This property holds the local time the user code and
136 underlying device codes expire. The codes are typically
137 valid between 5 and 30 minutes.
138
139 \sa userCode
140*/
141
142/*!
143 \fn void QOAuth2DeviceAuthorizationFlow::authorizeWithUserCode(
144 const QUrl &verificationUrl,
145 const QString &userCode,
146 const QUrl &completeVerificationUrl)
147
148 This signal is emitted when user should complete the authorization.
149
150 If authorization server has provided \a completeVerificationUrl,
151 user can navigate to that URL. The URL contains the needed \a userCode
152 and any other needed parameters.
153
154 Alternatively, the user needs to navigate to \a verificationUrl
155 and enter \a userCode manually.
156
157 \sa {Device Flow Usage}
158*/
159
160QOAuth2DeviceAuthorizationFlowPrivate::QOAuth2DeviceAuthorizationFlowPrivate(
161 QNetworkAccessManager *manager)
162 : QAbstractOAuth2Private(std::make_pair(x: QString(), y: QString()), QString(), manager)
163{
164}
165
166QOAuth2DeviceAuthorizationFlowPrivate::~QOAuth2DeviceAuthorizationFlowPrivate()
167{
168 resetCurrentAuthorizationReply();
169 resetCurrentTokenReply();
170 tokenPollingTimer.stop();
171}
172
173void QOAuth2DeviceAuthorizationFlowPrivate::authorizationReplyFinished(QRestReply &reply)
174{
175 Q_Q(QOAuth2DeviceAuthorizationFlow);
176
177 if (status != Status::NotAuthenticated) {
178 logAuthorizationStageWarning(message: "reply finished in unexpected flow status"_L1,
179 detail: static_cast<int>(status));
180 return;
181 }
182 // HTTP status is not checked, because, while RFC states that 400 (Bad Request)
183 // should be used, it also says 'unless specified otherwise'. And indeed authorization
184 // servers do use different statuses (eg. 428). So we only concern ourselves
185 // with network errors and what the body data says.
186 // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
187 if (reply.hasError()) {
188 logAuthorizationStageWarning(message: "network error"_L1);
189 emit q->requestFailed(error: Error::NetworkError);
190 return;
191 }
192 const auto jsonDoc = reply.readJson();
193 if (!jsonDoc || !jsonDoc->isObject()) {
194 logAuthorizationStageWarning(message: "invalid response format"_L1);
195 emit q->requestFailed(error: Error::ServerError);
196 return;
197 }
198 const auto data = jsonDoc->object();
199 if (handleRfcErrorResponseIfPresent(data: data.toVariantMap()))
200 return;
201
202 // https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 REQUIRED parameters
203 const auto receivedDeviceCode = data.value(key: QtOAuth2RfcKeywords::deviceCode).toString();
204 auto receivedUserCode = data.value(key: QtOAuth2RfcKeywords::userCode).toString();
205 const auto receivedExpiresIn = data.value(key: QtOAuth2RfcKeywords::expiresIn).toInt();
206 QUrl receivedVerificationUrl;
207 // The RFC keyword is 'verification_uri', but some auth servers provide 'verification_url'
208 if (data.contains(key: QtOAuth2RfcKeywords::verificationUri))
209 receivedVerificationUrl = data.value(key: QtOAuth2RfcKeywords::verificationUri).toString();
210 else if (data.contains(key: QtOAuth2RfcKeywords::verificationUrl))
211 receivedVerificationUrl = data.value(key: QtOAuth2RfcKeywords::verificationUrl).toString();
212
213 if (receivedDeviceCode.isEmpty() || receivedUserCode.isEmpty()
214 || receivedVerificationUrl.isEmpty() || receivedExpiresIn <= 0) {
215 logAuthorizationStageWarning(message: "required data not received"_L1);
216 emit q->requestFailed(error: Error::OAuthTokenNotFoundError);
217 return;
218 }
219
220 const int receivedMinimumInterval = data.value(key: QtOAuth2RfcKeywords::interval).toInt();
221 if (receivedMinimumInterval > 0) {
222 if (useAutoTestDurations)
223 tokenPollingTimer.setInterval(std::chrono::milliseconds(receivedMinimumInterval));
224 else
225 tokenPollingTimer.setInterval(std::chrono::seconds(receivedMinimumInterval));
226 } else {
227 tokenPollingTimer.setInterval(defaultPollingInterval);
228 }
229
230 // Store the expiration time
231 QDateTime newCodeExpiration;
232 if (useAutoTestDurations)
233 newCodeExpiration = QDateTime::currentDateTimeUtc().addMSecs(msecs: receivedExpiresIn);
234 else
235 newCodeExpiration = QDateTime::currentDateTimeUtc().addSecs(secs: receivedExpiresIn);
236 setUserCodeExpiration(newCodeExpiration);
237
238 if (isNextPollAfterExpiration()) {
239 logAuthorizationStageWarning(message: "code expired"_L1);
240 emit q->requestFailed(error: Error::ExpiredError);
241 return;
242 }
243
244 QUrl receivedVerificationUrlComplete;
245 // The RFC keyword is 'verification_uri_complete', but some auth servers
246 // use 'verification_url_complete'
247 if (data.contains(key: QtOAuth2RfcKeywords::completeVerificationUri)) {
248 receivedVerificationUrlComplete =
249 data.value(key: QtOAuth2RfcKeywords::completeVerificationUri).toString();
250 } else if (data.contains(key: QtOAuth2RfcKeywords::completeVerificationUrl)) {
251 receivedVerificationUrlComplete =
252 data.value(key: QtOAuth2RfcKeywords::completeVerificationUrl).toString();
253 }
254
255 deviceCode = std::move(receivedDeviceCode);
256 setUserCode(receivedUserCode);
257 setVerificationUrl(receivedVerificationUrl);
258 setVerificationUrlComplete(receivedVerificationUrlComplete);
259
260 QVariantMap copy(data.toVariantMap());
261 copy.remove(key: QtOAuth2RfcKeywords::deviceCode);
262 copy.remove(key: QtOAuth2RfcKeywords::userCode);
263 copy.remove(key: QtOAuth2RfcKeywords::verificationUrl);
264 copy.remove(key: QtOAuth2RfcKeywords::completeVerificationUrl);
265 setExtraTokens(copy);
266
267 setStatus(Status::TemporaryCredentialsReceived);
268 // Signal that user needs to authorize next, and start polling for tokens
269 emit q->authorizeWithUserCode(verificationUrl, userCode, completeVerificationUrl);
270 (void)startTokenPolling();
271}
272
273void QOAuth2DeviceAuthorizationFlowPrivate::tokenReplyFinished(QRestReply &reply)
274{
275 // HTTP status is not checked, because, while RFC states that 400 (Bad Request)
276 // should be used, it also says 'unless specified otherwise'. And indeed authorization
277 // servers do use different statuses (eg. 428). So we only concern ourselves
278 // with network errors and the body data.
279 // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
280 if (reply.hasError()) {
281 tokenAcquisitionFailed(error: Error::NetworkError, errorString: reply.errorString());
282 return;
283 }
284 const auto jsonDoc = reply.readJson();
285 if (!jsonDoc || !jsonDoc->isObject()) {
286 tokenAcquisitionFailed(error: Error::ServerError, errorString: u"Invalid response format"_s);
287 return;
288 }
289 const auto data = jsonDoc->object();
290 if (data.contains(key: QtOAuth2RfcKeywords::error)) {
291 // With device flow, error responses can be part of a successful flow
292 handleTokenErrorResponse(data);
293 return;
294 }
295 handleTokenSuccessResponse(data);
296}
297
298void QOAuth2DeviceAuthorizationFlowPrivate::handleTokenErrorResponse(const QJsonObject &data)
299{
300 Q_Q(QOAuth2DeviceAuthorizationFlow);
301 const auto errorCode = data.value(key: QtOAuth2RfcKeywords::error).toString();
302
303 // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
304 // RFC defines additional error codes that require specific handling
305 if (errorCode == "authorization_pending"_L1) {
306 // User has not yet authorized, keep polling
307 //qCDebug(loggingCategory) << "Not yet authorized, polling again in:"
308 // << tokenPollingTimer.interval();
309 } else if (errorCode == "slow_down"_L1) {
310 // Polling needs to slow down by (at least) 5 seconds
311 if (useAutoTestDurations)
312 tokenPollingTimer.setInterval(tokenPollingTimer.interval() + 50ms);
313 else
314 tokenPollingTimer.setInterval(tokenPollingTimer.interval() + 5s);
315 qCDebug(loggingCategory) << "Slow down requested, polling again in"
316 << std::chrono::duration_cast<std::chrono::milliseconds>(d: tokenPollingTimer.interval());
317 } else {
318 // Other errors are terminal
319 // https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
320 // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
321 const auto error = data.value(key: QtOAuth2RfcKeywords::error).toString();
322 const auto description = data.value(key: QtOAuth2RfcKeywords::errorDescription).toString();
323 const auto uri = data.value(key: QtOAuth2RfcKeywords::errorUri).toString();
324 qCDebug(loggingCategory) << "Token acquisition failed:" << error << description;
325#if QT_DEPRECATED_SINCE(6, 13)
326 QT_IGNORE_DEPRECATIONS(Q_EMIT q->error(error, description, uri);)
327#endif
328 Q_EMIT q->serverReportedErrorOccurred(error, errorDescription: description, uri);
329 if (errorCode == "expired_token"_L1)
330 tokenAcquisitionFailed(error: Error::ExpiredError, errorString: description);
331 else
332 tokenAcquisitionFailed(error: Error::ServerError, errorString: description);
333 }
334}
335
336void QOAuth2DeviceAuthorizationFlowPrivate::tokenAcquisitionFailed(QAbstractOAuth::Error error,
337 const QString &errorString)
338{
339 _q_tokenRequestFailed(error, errorString);
340 stopTokenPolling();
341}
342
343void QOAuth2DeviceAuthorizationFlowPrivate::setUserCode(const QString &code)
344{
345 Q_Q(QOAuth2DeviceAuthorizationFlow);
346 if (userCode == code)
347 return;
348 userCode = code;
349 emit q->userCodeChanged(userCode);
350}
351
352void QOAuth2DeviceAuthorizationFlowPrivate::setVerificationUrl(const QUrl &url)
353{
354 Q_Q(QOAuth2DeviceAuthorizationFlow);
355 if (verificationUrl == url)
356 return;
357 verificationUrl = url;
358 emit q->verificationUrlChanged(verificationUrl);
359}
360
361void QOAuth2DeviceAuthorizationFlowPrivate::setVerificationUrlComplete(const QUrl &url)
362{
363 Q_Q(QOAuth2DeviceAuthorizationFlow);
364 if (completeVerificationUrl == url)
365 return;
366 completeVerificationUrl = url;
367 emit q->completeVerificationUrlChanged(completeVerificationUrl);
368}
369
370void QOAuth2DeviceAuthorizationFlowPrivate::setUserCodeExpiration(const QDateTime &expiration)
371{
372 Q_Q(QOAuth2DeviceAuthorizationFlow);
373 Q_ASSERT(!expiration.isValid() || expiration.timeSpec() == Qt::TimeSpec::UTC);
374 if (userCodeExpirationUtc == expiration)
375 return;
376 userCodeExpirationUtc = expiration;
377 emit q->userCodeExpirationAtChanged(expiration: userCodeExpirationUtc.toLocalTime());
378}
379
380bool QOAuth2DeviceAuthorizationFlowPrivate::startTokenPolling()
381{
382 Q_Q(QOAuth2DeviceAuthorizationFlow);
383
384 if (q->isPolling()) {
385 qCDebug(loggingCategory, "Token stage: polling already active");
386 return true;
387 }
388 if (deviceCode.isEmpty()) {
389 logTokenStageWarning(message: "missing device code for polling"_L1);
390 emit q->requestFailed(error: Error::ClientError);
391 return false;
392 }
393 if (tokenUrl.isEmpty()) {
394 logTokenStageWarning(message: "token URL is empty"_L1);
395 emit q->requestFailed(error: Error::ClientError);
396 return false;
397 }
398 if (isNextPollAfterExpiration()) {
399 logTokenStageWarning(message: "code expired"_L1);
400 emit q->requestFailed(error: Error::ExpiredError);
401 return false;
402 }
403
404 qCDebug(loggingCategory) << "Token stage: starting polling with interval:"
405 << std::chrono::duration_cast<std::chrono::milliseconds>(d: tokenPollingTimer.interval());
406 tokenPollingTimer.start();
407 emit q->pollingChanged(polling: true);
408 return true;
409}
410
411void QOAuth2DeviceAuthorizationFlowPrivate::stopTokenPolling()
412{
413 Q_Q(QOAuth2DeviceAuthorizationFlow);
414 if (!q->isPolling())
415 return;
416
417 qCDebug(loggingCategory, "Token stage: Stopping token polling");
418 resetCurrentTokenReply();
419 tokenPollingTimer.stop();
420 emit q->pollingChanged(polling: false);
421}
422
423void QOAuth2DeviceAuthorizationFlowPrivate::pollTokens()
424{
425 Q_Q(QOAuth2DeviceAuthorizationFlow);
426 if (currentTokenReply) {
427 logTokenStageWarning(message: "poll request already in progress"_L1);
428 return;
429 }
430 if (tokenUrl.isEmpty()) {
431 tokenAcquisitionFailed(error: Error::ClientError, errorString: u"token URL is empty"_s);
432 return;
433 }
434 if (QDateTime::currentDateTimeUtc() >= userCodeExpirationUtc) {
435 tokenAcquisitionFailed(error: Error::ExpiredError, errorString: u"code expired"_s);
436 return;
437 }
438
439 QMultiMap<QString, QVariant> parameters;
440 // https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
441 static constexpr auto grantType = "urn:ietf:params:oauth:grant-type:device_code"_L1;
442 parameters.insert(key: QtOAuth2RfcKeywords::grantType, value: QUrl::toPercentEncoding(grantType));
443 parameters.insert(key: QtOAuth2RfcKeywords::deviceCode, value: QUrl::toPercentEncoding(deviceCode));
444 parameters.insert(key: QtOAuth2RfcKeywords::clientIdentifier,
445 value: QUrl::toPercentEncoding(clientIdentifier));
446 if (!clientIdentifierSharedKey.isEmpty())
447 parameters.insert(key: QtOAuth2RfcKeywords::clientSharedSecret, value: clientIdentifierSharedKey);
448 if (modifyParametersFunction)
449 modifyParametersFunction(QAbstractOAuth2::Stage::RequestingAccessToken, &parameters);
450
451 QUrlQuery query;
452 for (const auto &[key, value] : std::as_const(t&: parameters).asKeyValueRange())
453 query.addQueryItem(key, value: value.toString());
454
455 QNetworkRequest request(tokenUrl);
456 QHttpHeaders headers;
457 headers.append(name: QHttpHeaders::WellKnownHeader::ContentType,
458 value: "application/x-www-form-urlencoded"_L1);
459 request.setHeaders(headers);
460#ifndef QT_NO_SSL
461 if (sslConfiguration && !sslConfiguration->isNull())
462 request.setSslConfiguration(*sslConfiguration);
463#endif
464 callNetworkRequestModifier(request, stage: Stage::RequestingAccessToken);
465
466 const QByteArray data = query.toString(encoding: QUrl::FullyEncoded).toLatin1();
467 currentTokenReply = network()->post(request, data, context: q, callback: [this](QRestReply &reply) {
468 if (reply.networkReply() != currentTokenReply) {
469 logTokenStageWarning(message: "unexpected token reply"_L1);
470 return;
471 }
472 qCDebug(loggingCategory, "Token stage: token reply finished");
473 currentTokenReply->deleteLater();
474 currentTokenReply.clear();
475 tokenReplyFinished(reply);
476 });
477}
478
479void QOAuth2DeviceAuthorizationFlowPrivate::reset()
480{
481 Q_Q(QOAuth2DeviceAuthorizationFlow);
482 resetCurrentAuthorizationReply();
483 resetCurrentTokenReply();
484 setUserCode({});
485 setVerificationUrl({});
486 setVerificationUrlComplete({});
487 setUserCodeExpiration(QDateTime());
488 setExtraTokens({});
489 setExpiresAt(QDateTime());
490 deviceCode.clear();
491 if (q->isPolling()) {
492 tokenPollingTimer.stop();
493 emit q->pollingChanged(polling: false);
494 }
495 tokenPollingTimer.setInterval(defaultPollingInterval);
496 setStatus(Status::NotAuthenticated);
497}
498
499bool QOAuth2DeviceAuthorizationFlowPrivate::isNextPollAfterExpiration() const
500{
501 if (!userCodeExpirationUtc.isValid())
502 return true;
503
504 const QDateTime nextPoll = QDateTime::currentDateTimeUtc().addDuration(
505 msecs: std::chrono::duration_cast<std::chrono::milliseconds>(d: tokenPollingTimer.interval()));
506
507 return nextPoll > userCodeExpirationUtc;
508}
509
510void QOAuth2DeviceAuthorizationFlowPrivate::resetCurrentTokenReply()
511{
512 if (currentTokenReply) {
513 const auto reply = currentTokenReply.get();
514 // Clear current reply before abort to avoid handling the finished signal
515 currentTokenReply.clear();
516 reply->abort();
517 reply->deleteLater();
518 }
519}
520
521void QOAuth2DeviceAuthorizationFlowPrivate::resetCurrentAuthorizationReply()
522{
523 if (currentAuthorizationReply) {
524 const auto reply = currentAuthorizationReply.get();
525 // Clear current reply before abort to avoid handling the finished signal
526 currentAuthorizationReply.clear();
527 reply->abort();
528 reply->deleteLater();
529 }
530}
531
532QRestAccessManager *QOAuth2DeviceAuthorizationFlowPrivate::network()
533{
534 Q_Q(QOAuth2DeviceAuthorizationFlow);
535 if (!restAccessManager) {
536 // First usage, create
537 restAccessManager = new QRestAccessManager(networkAccessManager(), q);
538 } else if (restAccessManager->networkAccessManager() != networkAccessManager()) {
539 // QNetworkAccessManager has changed, re-create
540 resetCurrentAuthorizationReply();
541 resetCurrentTokenReply();
542 delete restAccessManager;
543 restAccessManager = new QRestAccessManager(networkAccessManager(), q);
544 }
545 return restAccessManager;
546}
547
548void QOAuth2DeviceAuthorizationFlowPrivate::handleTokenSuccessResponse(const QJsonObject &data)
549{
550 _q_tokenRequestFinished(values: data.toVariantMap());
551 stopTokenPolling();
552}
553
554/*!
555 Constructs a QOAuth2DeviceAuthorizationFlow object.
556*/
557QOAuth2DeviceAuthorizationFlow::QOAuth2DeviceAuthorizationFlow()
558 : QOAuth2DeviceAuthorizationFlow(static_cast<QObject*>(nullptr))
559{
560}
561
562/*!
563 Constructs a QOAuth2DeviceAuthorizationFlow object with parent
564 object \a parent.
565*/
566QOAuth2DeviceAuthorizationFlow::QOAuth2DeviceAuthorizationFlow(QObject *parent)
567 : QOAuth2DeviceAuthorizationFlow(nullptr, parent)
568{
569}
570
571/*!
572 Constructs a QOAuth2DeviceAuthorizationFlow object using \a parent
573 as parent and sets \a manager as the network access manager.
574*/
575QOAuth2DeviceAuthorizationFlow::QOAuth2DeviceAuthorizationFlow(QNetworkAccessManager *manager,
576 QObject *parent)
577 : QAbstractOAuth2(*new QOAuth2DeviceAuthorizationFlowPrivate(manager), parent)
578{
579 Q_D(QOAuth2DeviceAuthorizationFlow);
580 d->tokenPollingTimer.setInterval(d->defaultPollingInterval);
581 d->tokenPollingTimer.setSingleShot(false);
582 connect(sender: &d->tokenPollingTimer, signal: &QChronoTimer::timeout, context: this, slot: [d]() {
583 d->pollTokens();
584 });
585}
586
587/*!
588 Destroys the QOAuth2DeviceAuthorizationFlow instance.
589*/
590QOAuth2DeviceAuthorizationFlow::~QOAuth2DeviceAuthorizationFlow()
591 = default;
592
593QString QOAuth2DeviceAuthorizationFlow::userCode() const
594{
595 Q_D(const QOAuth2DeviceAuthorizationFlow);
596 return d->userCode;
597}
598
599QUrl QOAuth2DeviceAuthorizationFlow::verificationUrl() const
600{
601 Q_D(const QOAuth2DeviceAuthorizationFlow);
602 return d->verificationUrl;
603}
604
605QUrl QOAuth2DeviceAuthorizationFlow::completeVerificationUrl() const
606{
607 Q_D(const QOAuth2DeviceAuthorizationFlow);
608 return d->completeVerificationUrl;
609}
610
611bool QOAuth2DeviceAuthorizationFlow::isPolling() const
612{
613 Q_D(const QOAuth2DeviceAuthorizationFlow);
614 return d->tokenPollingTimer.isActive();
615}
616
617QDateTime QOAuth2DeviceAuthorizationFlow::userCodeExpirationAt() const
618{
619 Q_D(const QOAuth2DeviceAuthorizationFlow);
620 return d->userCodeExpirationUtc.toLocalTime();
621}
622
623/*!
624 Starts the authorization flow as described in
625 \l {https://datatracker.ietf.org/doc/html/rfc8628#section-3}{Device Grant RFC}.
626
627 The flow consists of following steps:
628 \list
629 \li Authorization request to the authorization server
630 \li User authorizing the access, see \l {authorizeWithUserCode()}
631 \li Polling the authorization server until user has accepted
632 or rejected the authorization (or codes expire)
633 \li Indicating the result to the application (see granted() and
634 QAbstractOAuth::requestFailed())
635 \endlist
636 The flow progresses automatically from authorization to token polling.
637
638 Calling this function will reset any previous authorization data.
639
640 \sa authorizeWithUserCode(), granted(), QAbstractOAuth::requestFailed(),
641 polling, startTokenPolling(), stopTokenPolling(), {Device Flow Usage}
642*/
643void QOAuth2DeviceAuthorizationFlow::grant()
644{
645 Q_D(QOAuth2DeviceAuthorizationFlow);
646 d->reset();
647
648 if (d->authorizationUrl.isEmpty()) {
649 d->logAuthorizationStageWarning(message: "No authorization URL set"_L1);
650 emit requestFailed(error: Error::ClientError);
651 return;
652 }
653 if (d->tokenUrl.isEmpty()) {
654 d->logAuthorizationStageWarning(message: "No token URL set"_L1);
655 emit requestFailed(error: Error::ClientError);
656 return;
657 }
658
659 QMultiMap<QString, QVariant> parameters;
660 parameters.insert(key: QtOAuth2RfcKeywords::clientIdentifier, value: d->clientIdentifier);
661#ifndef QOAUTH2_NO_LEGACY_SCOPE
662 if (d->legacyScopeWasSetByUser) {
663 if (!d->legacyScope.isEmpty())
664 parameters.insert(key: QtOAuth2RfcKeywords::scope, value: d->legacyScope);
665 } else
666#endif
667 if (!d->requestedScopeTokens.isEmpty())
668 parameters.insert(key: QtOAuth2RfcKeywords::scope, value: d->joinedScope(scopeTokens: d->requestedScopeTokens));
669 if (d->authorizationShouldIncludeNonce()) {
670 if (d->nonce.isEmpty())
671 setNonce(QAbstractOAuth2Private::generateNonce());
672 parameters.insert(key: QtOAuth2RfcKeywords::nonce, value: d->nonce);
673 }
674 if (d->modifyParametersFunction)
675 d->modifyParametersFunction(Stage::RequestingAuthorization, &parameters);
676
677 QUrlQuery query;
678 for (const auto &[key, value] : std::as_const(t&: parameters).asKeyValueRange())
679 query.addQueryItem(key, value: value.toString());
680
681 QNetworkRequest request(d->authorizationUrl);
682 QHttpHeaders headers;
683 headers.append(name: QHttpHeaders::WellKnownHeader::ContentType,
684 value: "application/x-www-form-urlencoded"_L1);
685 request.setHeaders(headers);
686
687#ifndef QT_NO_SSL
688 if (d->sslConfiguration && !d->sslConfiguration->isNull())
689 request.setSslConfiguration(*d->sslConfiguration);
690#endif
691 d->callNetworkRequestModifier(request, stage: Stage::RequestingAuthorization);
692
693 const QByteArray data = query.toString(encoding: QUrl::FullyEncoded).toLatin1();
694 d->currentAuthorizationReply =
695 d->network()->post(request, data, context: this, callback: [d](QRestReply &reply) {
696 if (reply.networkReply() != d->currentAuthorizationReply) {
697 d->logAuthorizationStageWarning(message: "unexpected reply"_L1);
698 return;
699 }
700 qCDebug(d->loggingCategory, "Authorization stage: reply finished");
701 d->currentAuthorizationReply->deleteLater();
702 d->currentAuthorizationReply.clear();
703 d->authorizationReplyFinished(reply);
704 });
705}
706
707/*!
708 \since 6.9
709
710 This function sends a token refresh request.
711
712 If the refresh request was initiated successfully, the status is set to
713 \l QAbstractOAuth::Status::RefreshingToken; otherwise the \l requestFailed()
714 signal is emitted and the status is not changed. Tokens cannot be refreshed
715 while \l {isPolling} is \c {true}.
716
717 This function has no effect if the token refresh process is already in
718 progress.
719
720 If refreshing the token fails and an access token exists, the status is
721 set to \l QAbstractOAuth::Status::Granted, and to
722 \l QAbstractOAuth::Status::NotAuthenticated if an access token
723 does not exist.
724
725 \sa QAbstractOAuth::requestFailed(), QAbstractOAuth2::refreshTokens()
726*/
727void QOAuth2DeviceAuthorizationFlow::refreshTokensImplementation()
728{
729 Q_D(QOAuth2DeviceAuthorizationFlow);
730 if (d->status == Status::RefreshingToken && d->currentTokenReply) {
731 qCDebug(d->loggingCategory, "refresh already in progress");
732 return;
733 }
734 if (isPolling()) {
735 d->logTokenStageWarning(message: "polling in progress, cannot refresh"_L1);
736 emit requestFailed(error: Error::ClientError);
737 return;
738 }
739 if (d->refreshToken.isEmpty()) {
740 d->logTokenStageWarning(message: "empty refresh token"_L1);
741 emit requestFailed(error: Error::ClientError);
742 return;
743 }
744 if (d->tokenUrl.isEmpty()) {
745 d->logTokenStageWarning(message: "No token URL set"_L1);
746 emit requestFailed(error: Error::ClientError);
747 return;
748 }
749
750 d->resetCurrentTokenReply();
751
752 const auto [request, body] = d->createRefreshRequestAndBody(url: d->tokenUrl);
753 d->currentTokenReply = d->network()->post(request, data: body, context: this, callback: [d](QRestReply &reply) {
754 if (reply.networkReply() != d->currentTokenReply) {
755 d->logTokenStageWarning(message: "unexpected refresh reply"_L1);
756 return;
757 }
758 qCDebug(d->loggingCategory, "Token stage: refresh reply finished");
759 d->currentTokenReply->deleteLater();
760 d->currentTokenReply.clear();
761 d->tokenReplyFinished(reply);
762 });
763 setStatus(Status::RefreshingToken);
764}
765
766/*!
767 Starts token polling. Returns \c {true} if the start
768 was successful (or was already active), and \c {false} otherwise.
769
770 Calling this function is not necessary in a typical use case.
771 Once the authorization request has completed,
772 as a result of \l {grant()} call, the polling is started automatically.
773
774 This function can be useful in cases where resuming (retrying)
775 the token polling a bit later is needed, without restarting
776 the whole authorization flow. For example in case of a transient
777 network connectivity loss.
778
779 Polling interval is defined by the authorization server, and is
780 typically 5 seconds. First poll request is sent once the first interval
781 has elapsed.
782
783 \sa polling, stopTokenPolling(), {Device Flow Usage}
784 */
785bool QOAuth2DeviceAuthorizationFlow::startTokenPolling()
786{
787 Q_D(QOAuth2DeviceAuthorizationFlow);
788 return d->startTokenPolling();
789}
790
791/*!
792 Stops token polling. Any potential outstanding poll requests
793 are silently discarded.
794
795 \sa polling, startTokenPolling()
796 */
797void QOAuth2DeviceAuthorizationFlow::stopTokenPolling()
798{
799 Q_D(QOAuth2DeviceAuthorizationFlow);
800 d->stopTokenPolling();
801}
802
803bool QOAuth2DeviceAuthorizationFlow::event(QEvent* event)
804{
805 // https://wiki.qt.io/Things_To_Look_Out_For_In_Reviews#New_Classes
806 return QAbstractOAuth2::event(event);
807}
808
809QT_END_NAMESPACE
810
811#include "moc_qoauth2deviceauthorizationflow.cpp"
812

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