| 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 | |
| 22 | QT_BEGIN_NAMESPACE |
| 23 | |
| 24 | using namespace Qt::StringLiterals; |
| 25 | using namespace std::chrono_literals; |
| 26 | using Error = QAbstractOAuth::Error; |
| 27 | using Status = QAbstractOAuth::Status; |
| 28 | using 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 | |
| 160 | QOAuth2DeviceAuthorizationFlowPrivate::QOAuth2DeviceAuthorizationFlowPrivate( |
| 161 | QNetworkAccessManager *manager) |
| 162 | : QAbstractOAuth2Private(std::make_pair(x: QString(), y: QString()), QString(), manager) |
| 163 | { |
| 164 | } |
| 165 | |
| 166 | QOAuth2DeviceAuthorizationFlowPrivate::~QOAuth2DeviceAuthorizationFlowPrivate() |
| 167 | { |
| 168 | resetCurrentAuthorizationReply(); |
| 169 | resetCurrentTokenReply(); |
| 170 | tokenPollingTimer.stop(); |
| 171 | } |
| 172 | |
| 173 | void 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 | |
| 273 | void 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 | |
| 298 | void 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 | |
| 336 | void QOAuth2DeviceAuthorizationFlowPrivate::tokenAcquisitionFailed(QAbstractOAuth::Error error, |
| 337 | const QString &errorString) |
| 338 | { |
| 339 | _q_tokenRequestFailed(error, errorString); |
| 340 | stopTokenPolling(); |
| 341 | } |
| 342 | |
| 343 | void 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 | |
| 352 | void 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 | |
| 361 | void 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 | |
| 370 | void 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 | |
| 380 | bool 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 | |
| 411 | void 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 | |
| 423 | void 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, ¶meters); |
| 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 ; |
| 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 | |
| 479 | void 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 | |
| 499 | bool 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 | |
| 510 | void 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 | |
| 521 | void 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 | |
| 532 | QRestAccessManager *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 | |
| 548 | void QOAuth2DeviceAuthorizationFlowPrivate::handleTokenSuccessResponse(const QJsonObject &data) |
| 549 | { |
| 550 | _q_tokenRequestFinished(values: data.toVariantMap()); |
| 551 | stopTokenPolling(); |
| 552 | } |
| 553 | |
| 554 | /*! |
| 555 | Constructs a QOAuth2DeviceAuthorizationFlow object. |
| 556 | */ |
| 557 | QOAuth2DeviceAuthorizationFlow::QOAuth2DeviceAuthorizationFlow() |
| 558 | : QOAuth2DeviceAuthorizationFlow(static_cast<QObject*>(nullptr)) |
| 559 | { |
| 560 | } |
| 561 | |
| 562 | /*! |
| 563 | Constructs a QOAuth2DeviceAuthorizationFlow object with parent |
| 564 | object \a parent. |
| 565 | */ |
| 566 | QOAuth2DeviceAuthorizationFlow::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 | */ |
| 575 | QOAuth2DeviceAuthorizationFlow::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 | */ |
| 590 | QOAuth2DeviceAuthorizationFlow::~QOAuth2DeviceAuthorizationFlow() |
| 591 | = default; |
| 592 | |
| 593 | QString QOAuth2DeviceAuthorizationFlow::userCode() const |
| 594 | { |
| 595 | Q_D(const QOAuth2DeviceAuthorizationFlow); |
| 596 | return d->userCode; |
| 597 | } |
| 598 | |
| 599 | QUrl QOAuth2DeviceAuthorizationFlow::verificationUrl() const |
| 600 | { |
| 601 | Q_D(const QOAuth2DeviceAuthorizationFlow); |
| 602 | return d->verificationUrl; |
| 603 | } |
| 604 | |
| 605 | QUrl QOAuth2DeviceAuthorizationFlow::completeVerificationUrl() const |
| 606 | { |
| 607 | Q_D(const QOAuth2DeviceAuthorizationFlow); |
| 608 | return d->completeVerificationUrl; |
| 609 | } |
| 610 | |
| 611 | bool QOAuth2DeviceAuthorizationFlow::isPolling() const |
| 612 | { |
| 613 | Q_D(const QOAuth2DeviceAuthorizationFlow); |
| 614 | return d->tokenPollingTimer.isActive(); |
| 615 | } |
| 616 | |
| 617 | QDateTime 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 | */ |
| 643 | void 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, ¶meters); |
| 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 ; |
| 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 | */ |
| 727 | void 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 | */ |
| 785 | bool 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 | */ |
| 797 | void QOAuth2DeviceAuthorizationFlow::stopTokenPolling() |
| 798 | { |
| 799 | Q_D(QOAuth2DeviceAuthorizationFlow); |
| 800 | d->stopTokenPolling(); |
| 801 | } |
| 802 | |
| 803 | bool 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 | |
| 809 | QT_END_NAMESPACE |
| 810 | |
| 811 | #include "moc_qoauth2deviceauthorizationflow.cpp" |
| 812 | |