|
34 | 34 | #include <QNetworkReply> |
35 | 35 | #include <QPixmap> |
36 | 36 | #include <QRandomGenerator> |
| 37 | +#include <ranges> |
37 | 38 |
|
38 | 39 | using namespace std::chrono; |
39 | 40 | using namespace std::chrono_literals; |
@@ -537,68 +538,128 @@ void OAuth::fetchWellKnown() |
537 | 538 | _wellKnownFinished = true; |
538 | 539 | Q_EMIT fetchWellKnownFinished(); |
539 | 540 | } else { |
540 | | - qCDebug(lcOauth) << u"fetching" << wellKnownPathC; |
| 541 | + QNetworkRequest webfingerReq; |
| 542 | + webfingerReq.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); |
| 543 | + webfingerReq.setUrl( |
| 544 | + Utility::concatUrlPath(_serverUrl, QStringLiteral("/.well-known/webfinger"), {{QStringLiteral("resource"), _serverUrl.toString()}})); |
| 545 | + webfingerReq.setTransferTimeout(defaultTimeoutMs()); |
541 | 546 |
|
542 | | - QNetworkRequest req; |
543 | | - req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); |
544 | | - req.setUrl(Utility::concatUrlPath(_serverUrl, wellKnownPathC)); |
545 | | - req.setTransferTimeout(defaultTimeoutMs()); |
| 547 | + auto webfingerReply = _networkAccessManager->get(webfingerReq); |
546 | 548 |
|
547 | | - auto reply = _networkAccessManager->get(req); |
| 549 | + connect(webfingerReply, &QNetworkReply::finished, this, [webfingerReply, this] { |
| 550 | + if (webfingerReply->error() != QNetworkReply::NoError) { |
| 551 | + Q_EMIT result(Error); |
| 552 | + return; |
| 553 | + } |
548 | 554 |
|
549 | | - connect(reply, &QNetworkReply::finished, this, [reply, this] { |
550 | | - _wellKnownFinished = true; |
551 | | - if (reply->error() != QNetworkReply::NoError) { |
552 | | - qCDebug(lcOauth) << u"failed to fetch .well-known reply, error:" << reply->error(); |
553 | | - if (_isRefreshingToken) { |
554 | | - Q_EMIT refreshError(reply->error(), reply->errorString()); |
555 | | - } else { |
556 | | - Q_EMIT result(Error); |
557 | | - } |
| 555 | + const QString contentTypeHeader = webfingerReply->header(QNetworkRequest::ContentTypeHeader).toString(); |
| 556 | + if (!contentTypeHeader.contains(QStringLiteral("application/json"), Qt::CaseInsensitive)) { |
| 557 | + qCWarning(lcOauth) << u"server sent invalid content type:" << contentTypeHeader; |
| 558 | + Q_EMIT result(Error); |
558 | 559 | return; |
559 | 560 | } |
560 | | - QJsonParseError err = {}; |
561 | | - QJsonObject data = QJsonDocument::fromJson(reply->readAll(), &err).object(); |
562 | | - if (err.error == QJsonParseError::NoError) { |
563 | | - _authEndpoint = QUrl::fromEncoded(data[QStringLiteral("authorization_endpoint")].toString().toUtf8()); |
564 | | - _tokenEndpoint = QUrl::fromEncoded(data[QStringLiteral("token_endpoint")].toString().toUtf8()); |
565 | | - _registrationEndpoint = QUrl::fromEncoded(data[QStringLiteral("registration_endpoint")].toString().toUtf8()); |
566 | | - |
567 | | - if (_clientSecret.isEmpty()) { |
568 | | - _endpointAuthMethod = TokenEndpointAuthMethods::none; |
569 | | - } else { |
570 | | - const auto authMethods = data.value(QStringLiteral("token_endpoint_auth_methods_supported")).toArray(); |
571 | | - if (authMethods.contains(QStringLiteral("none"))) { |
572 | | - _endpointAuthMethod = TokenEndpointAuthMethods::none; |
573 | | - } else if (authMethods.contains(QStringLiteral("client_secret_post"))) { |
574 | | - _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_post; |
575 | | - } else if (authMethods.contains(QStringLiteral("client_secret_basic"))) { |
576 | | - _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic; |
| 561 | + |
| 562 | + QJsonParseError error; |
| 563 | + const auto doc = QJsonDocument::fromJson(webfingerReply->readAll(), &error); |
| 564 | + |
| 565 | + // empty or invalid response |
| 566 | + if (error.error != QJsonParseError::NoError || doc.isNull()) { |
| 567 | + qCWarning(lcOauth) << u"could not parse JSON response from server"; |
| 568 | + Q_EMIT result(Error); |
| 569 | + return; |
| 570 | + } |
| 571 | + |
| 572 | + // make sure the reported subject matches the requested resource |
| 573 | + const auto subject = doc.object().value(QStringLiteral("subject")); |
| 574 | + if (subject != _serverUrl.toString()) { |
| 575 | + qCWarning(lcOauth) << u"reply sent for different subject (server):" << subject; |
| 576 | + Q_EMIT result(Error); |
| 577 | + return; |
| 578 | + } |
| 579 | + |
| 580 | + // check for an OIDC issuer in the list of links provided (we use the first that matches our conditions) |
| 581 | + const auto links = doc.object().value(QStringLiteral("links")).toArray(); |
| 582 | + const auto objects = std::views::transform(links, [](const QJsonValueConstRef &object) { return object.toObject(); }); |
| 583 | + const auto link = std::ranges::find_if(objects, [](const QJsonObject &linkObject) { |
| 584 | + return linkObject.value(QStringLiteral("rel")).toString() == QStringLiteral("http://openid.net/specs/connect/1.0/issuer"); |
| 585 | + }); |
| 586 | + if (link == objects.end()) { |
| 587 | + qCWarning(lcOauth) << u"could not find suitable relation in WebFinger response"; |
| 588 | + Q_EMIT result(Error); |
| 589 | + return; |
| 590 | + } |
| 591 | + |
| 592 | + auto const issuerUrl = (*link).value(QStringLiteral("href")).toString(); |
| 593 | + if (issuerUrl.isNull()) { |
| 594 | + qCWarning(lcOauth) << u"could not find href in WebFinger response"; |
| 595 | + Q_EMIT result(Error); |
| 596 | + return; |
| 597 | + } |
| 598 | + |
| 599 | + auto const oidcWellKnownUrl = Utility::concatUrlPath(QUrl(issuerUrl), wellKnownPathC); |
| 600 | + qCDebug(lcOauth) << u"fetching" << oidcWellKnownUrl; |
| 601 | + |
| 602 | + QNetworkRequest req; |
| 603 | + req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); |
| 604 | + req.setUrl(oidcWellKnownUrl); |
| 605 | + req.setTransferTimeout(defaultTimeoutMs()); |
| 606 | + |
| 607 | + auto reply = _networkAccessManager->get(req); |
| 608 | + |
| 609 | + connect(reply, &QNetworkReply::finished, this, [reply, this] { |
| 610 | + _wellKnownFinished = true; |
| 611 | + if (reply->error() != QNetworkReply::NoError) { |
| 612 | + qCDebug(lcOauth) << u"failed to fetch .well-known reply, error:" << reply->error(); |
| 613 | + if (_isRefreshingToken) { |
| 614 | + Q_EMIT refreshError(reply->error(), reply->errorString()); |
577 | 615 | } else { |
578 | | - OC_ASSERT_X( |
579 | | - false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods)))); |
| 616 | + Q_EMIT result(Error); |
580 | 617 | } |
| 618 | + return; |
581 | 619 | } |
582 | | - const auto promtValuesSupported = data.value(QStringLiteral("prompt_values_supported")).toArray(); |
583 | | - if (!promtValuesSupported.isEmpty()) { |
584 | | - _supportedPromtValues = PromptValuesSupported::none; |
585 | | - for (const auto &x : promtValuesSupported) { |
586 | | - const auto flag = Utility::stringToEnum<PromptValuesSupported>(x.toString()); |
587 | | - // only use flags present in Theme::instance()->openIdConnectPrompt() |
588 | | - if (flag & defaultOauthPromtValue()) |
589 | | - _supportedPromtValues |= flag; |
| 620 | + QJsonParseError err = {}; |
| 621 | + QJsonObject data = QJsonDocument::fromJson(reply->readAll(), &err).object(); |
| 622 | + if (err.error == QJsonParseError::NoError) { |
| 623 | + _authEndpoint = QUrl::fromEncoded(data[QStringLiteral("authorization_endpoint")].toString().toUtf8()); |
| 624 | + _tokenEndpoint = QUrl::fromEncoded(data[QStringLiteral("token_endpoint")].toString().toUtf8()); |
| 625 | + _registrationEndpoint = QUrl::fromEncoded(data[QStringLiteral("registration_endpoint")].toString().toUtf8()); |
| 626 | + |
| 627 | + if (_clientSecret.isEmpty()) { |
| 628 | + _endpointAuthMethod = TokenEndpointAuthMethods::none; |
| 629 | + } else { |
| 630 | + const auto authMethods = data.value(QStringLiteral("token_endpoint_auth_methods_supported")).toArray(); |
| 631 | + if (authMethods.contains(QStringLiteral("none"))) { |
| 632 | + _endpointAuthMethod = TokenEndpointAuthMethods::none; |
| 633 | + } else if (authMethods.contains(QStringLiteral("client_secret_post"))) { |
| 634 | + _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_post; |
| 635 | + } else if (authMethods.contains(QStringLiteral("client_secret_basic"))) { |
| 636 | + _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic; |
| 637 | + } else { |
| 638 | + OC_ASSERT_X( |
| 639 | + false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods)))); |
| 640 | + } |
| 641 | + } |
| 642 | + const auto promtValuesSupported = data.value(QStringLiteral("prompt_values_supported")).toArray(); |
| 643 | + if (!promtValuesSupported.isEmpty()) { |
| 644 | + _supportedPromtValues = PromptValuesSupported::none; |
| 645 | + for (const auto &x : promtValuesSupported) { |
| 646 | + const auto flag = Utility::stringToEnum<PromptValuesSupported>(x.toString()); |
| 647 | + // only use flags present in Theme::instance()->openIdConnectPrompt() |
| 648 | + if (flag & defaultOauthPromtValue()) |
| 649 | + _supportedPromtValues |= flag; |
| 650 | + } |
590 | 651 | } |
591 | | - } |
592 | 652 |
|
593 | | - qCDebug(lcOauth) << u"parsing .well-known reply successful, auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint |
594 | | - << u"and registration endpoint" << _registrationEndpoint; |
595 | | - } else if (err.error == QJsonParseError::IllegalValue) { |
596 | | - qCDebug(lcOauth) << u"failed to parse .well-known reply as JSON, server might not support OIDC"; |
597 | | - } else { |
598 | | - qCDebug(lcOauth) << u"failed to parse .well-known reply, error:" << err.error; |
599 | | - Q_EMIT result(Error); |
600 | | - } |
601 | | - Q_EMIT fetchWellKnownFinished(); |
| 653 | + qCDebug(lcOauth) << u"parsing .well-known reply successful, auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint |
| 654 | + << u"and registration endpoint" << _registrationEndpoint; |
| 655 | + } else if (err.error == QJsonParseError::IllegalValue) { |
| 656 | + qCDebug(lcOauth) << u"failed to parse .well-known reply as JSON, server might not support OIDC"; |
| 657 | + } else { |
| 658 | + qCDebug(lcOauth) << u"failed to parse .well-known reply, error:" << err.error; |
| 659 | + Q_EMIT result(Error); |
| 660 | + } |
| 661 | + Q_EMIT fetchWellKnownFinished(); |
| 662 | + }); |
602 | 663 | }); |
603 | 664 | } |
604 | 665 | } |
|
0 commit comments