diff --git a/src/gui/newwizard/CMakeLists.txt b/src/gui/newwizard/CMakeLists.txt index eab47d6c0b..5e3761f66c 100644 --- a/src/gui/newwizard/CMakeLists.txt +++ b/src/gui/newwizard/CMakeLists.txt @@ -31,9 +31,6 @@ target_sources(OpenCloudGui PRIVATE jobs/resolveurljobfactory.cpp jobs/resolveurljobfactory.h - jobs/discoverwebfingerservicejobfactory.cpp - jobs/discoverwebfingerservicejobfactory.h - jobs/webfingeruserinfojobfactory.cpp jobs/webfingeruserinfojobfactory.h diff --git a/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp b/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp deleted file mode 100644 index 0b05e6858f..0000000000 --- a/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) Fabian Müller - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#include "discoverwebfingerservicejobfactory.h" - -#include "common/utility.h" - -#include -#include -#include -#include -#include - -namespace OCC::Wizard::Jobs { - -Q_LOGGING_CATEGORY(lcDiscoverWebFingerService, "gui.jobs.discoverwebfinger"); - -CoreJob *DiscoverWebFingerServiceJobFactory::startJob(const QUrl &url, QObject *parent) -{ - // this first request needs to be done without any authentication, since our goal is to find a server to authenticate to before the actual (authenticated) - // WebFinger request - auto req = makeRequest(Utility::concatUrlPath(url, QStringLiteral("/.well-known/webfinger"), {{QStringLiteral("resource"), url.toString()}})); - - auto *job = new CoreJob(nam()->get(req), parent); - - QObject::connect(job->reply(), &QNetworkReply::finished, job, [job, url]() { - auto setInvalidReplyError = [job]() { - setJobError(job, QApplication::translate("DiscoverWebFingerServiceJobFactory", "Invalid reply received from server")); - }; - - switch (job->reply()->error()) { - case QNetworkReply::NoError: - // all good, perform additional checks below - break; - default: - setInvalidReplyError(); - return; - } - - const QString contentTypeHeader = job->reply()->header(QNetworkRequest::ContentTypeHeader).toString(); - if (!contentTypeHeader.toLower().contains(QStringLiteral("application/json"))) { - qCWarning(lcDiscoverWebFingerService) << u"server sent invalid content type:" << contentTypeHeader; - setInvalidReplyError(); - return; - } - - // next up, parse JSON - QJsonParseError error; - const auto doc = QJsonDocument::fromJson(job->reply()->readAll(), &error); - // empty or invalid response - if (error.error != QJsonParseError::NoError || doc.isNull()) { - qCWarning(lcDiscoverWebFingerService) << u"could not parse JSON response from server"; - setInvalidReplyError(); - return; - } - - // make sure the reported subject matches the requested resource - const auto subject = doc.object().value(QStringLiteral("subject")); - if (subject != url.toString()) { - qCWarning(lcDiscoverWebFingerService) << u"reply sent for different subject (server):" << subject; - setInvalidReplyError(); - return; - } - - // check for an OIDC issuer in the list of links provided (we use the first that matches our conditions) - const auto links = doc.object().value(QStringLiteral("links")).toArray(); - for (const auto &link : links) { - const auto linkObject = link.toObject(); - - if (linkObject.value(QStringLiteral("rel")).toString() == QStringLiteral("http://openid.net/specs/connect/1.0/issuer")) { - // we have good faith in the server to provide a meaningful value and do not have to validate this any further - const auto href = linkObject.value(QStringLiteral("href")).toString(); - setJobResult(job, href); - return; - } - } - - qCWarning(lcDiscoverWebFingerService) << u"could not find suitable relation in WebFinger response"; - setInvalidReplyError(); - }); - - return job; -} - -DiscoverWebFingerServiceJobFactory::DiscoverWebFingerServiceJobFactory(QNetworkAccessManager *nam) - : AbstractCoreJobFactory(nam) -{ -} - -} diff --git a/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.h b/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.h deleted file mode 100644 index 02b55d3091..0000000000 --- a/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) Fabian Müller - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#pragma once - -#include "abstractcorejob.h" - -namespace OCC::Wizard::Jobs { - -/** - * Check whether we need to run an authenticated WebFinger request to find a user's list of allowed instances. - */ -class DiscoverWebFingerServiceJobFactory : public AbstractCoreJobFactory -{ -public: - DiscoverWebFingerServiceJobFactory(QNetworkAccessManager *nam); - - CoreJob *startJob(const QUrl &url, QObject *parent) override; -}; -} diff --git a/src/gui/newwizard/setupwizardaccountbuilder.cpp b/src/gui/newwizard/setupwizardaccountbuilder.cpp index 42ffc27c12..3029ded1bc 100644 --- a/src/gui/newwizard/setupwizardaccountbuilder.cpp +++ b/src/gui/newwizard/setupwizardaccountbuilder.cpp @@ -156,16 +156,6 @@ QString SetupWizardAccountBuilder::syncTargetDir() const return _defaultSyncTargetDir; } -void SetupWizardAccountBuilder::setWebFingerAuthenticationServerUrl(const QUrl &url) -{ - _webFingerAuthenticationServerUrl = url; -} - -QUrl SetupWizardAccountBuilder::webFingerAuthenticationServerUrl() const -{ - return _webFingerAuthenticationServerUrl; -} - void SetupWizardAccountBuilder::setWebFingerInstances(const QVector &instancesList) { _webFingerInstances = instancesList; diff --git a/src/gui/newwizard/setupwizardaccountbuilder.h b/src/gui/newwizard/setupwizardaccountbuilder.h index 589161d8c8..5ad00fdeb6 100644 --- a/src/gui/newwizard/setupwizardaccountbuilder.h +++ b/src/gui/newwizard/setupwizardaccountbuilder.h @@ -95,9 +95,6 @@ class SetupWizardAccountBuilder */ AccountPtr build() const; - void setWebFingerAuthenticationServerUrl(const QUrl &url); - QUrl webFingerAuthenticationServerUrl() const; - void setWebFingerInstances(const QVector &instancesList); QVector webFingerInstances() const; @@ -107,7 +104,6 @@ class SetupWizardAccountBuilder private: QUrl _serverUrl; - QUrl _webFingerAuthenticationServerUrl; QVector _webFingerInstances; QUrl _webFingerSelectedInstance; diff --git a/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp b/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp index c033ee55d7..eb1e3f469a 100644 --- a/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp +++ b/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp @@ -21,14 +21,7 @@ namespace OCC::Wizard { OAuthCredentialsSetupWizardState::OAuthCredentialsSetupWizardState(SetupWizardContext *context) : AbstractSetupWizardState(context) { - const auto authServerUrl = [this]() { - auto authServerUrl = _context->accountBuilder().webFingerAuthenticationServerUrl(); - if (!authServerUrl.isEmpty()) { - return authServerUrl; - } - return _context->accountBuilder().serverUrl(); - }(); - + const auto authServerUrl = _context->accountBuilder().serverUrl(); auto oAuth = new OAuth(authServerUrl, _context->accessManager(), {}, this); _page = new OAuthCredentialsSetupWizardPage(oAuth, authServerUrl); @@ -59,33 +52,27 @@ OAuthCredentialsSetupWizardState::OAuthCredentialsSetupWizardState(SetupWizardCo } }; - // SECOND WEBFINGER CALL (authenticated): // This discovers which OpenCloud instance(s) the authenticated user has access to. // Uses the OAuth bearer token and resource="acct:me@{host}". // Looking for: rel="http://webfinger.opencloud/rel/server-instance" - // See issue #271 for why we perform WebFinger twice. // Backend WebFinger docs: https://github.com/opencloud-eu/opencloud/blob/main/services/webfinger/README.md - if (!_context->accountBuilder().webFingerAuthenticationServerUrl().isEmpty()) { - auto *job = Jobs::WebFingerInstanceLookupJobFactory(_context->accessManager(), token).startJob(_context->accountBuilder().serverUrl(), this); + auto *job = Jobs::WebFingerInstanceLookupJobFactory(_context->accessManager(), token).startJob(_context->accountBuilder().serverUrl(), this); - connect(job, &CoreJob::finished, this, [finish, job, this]() { - if (!job->success()) { - Q_EMIT evaluationFailed(QStringLiteral("Failed to look up instances: %1").arg(job->errorMessage())); - } else { - const auto instanceUrls = qvariant_cast>(job->result()); + connect(job, &CoreJob::finished, this, [finish, job, this]() { + if (!job->success()) { + Q_EMIT evaluationFailed(QStringLiteral("Failed to look up instances: %1").arg(job->errorMessage())); + } else { + const auto instanceUrls = qvariant_cast>(job->result()); - if (instanceUrls.isEmpty()) { - Q_EMIT evaluationFailed(QStringLiteral("Server returned empty list of instances")); - } else { - _context->accountBuilder().setWebFingerInstances(instanceUrls); - } + if (instanceUrls.isEmpty()) { + Q_EMIT evaluationFailed(QStringLiteral("Server returned empty list of instances")); + } else { + _context->accountBuilder().setWebFingerInstances(instanceUrls); } + } - finish(); - }); - } else { finish(); - } + }); }); // the implementation moves to the next state automatically once ready, no user interaction needed diff --git a/src/gui/newwizard/states/serverurlsetupwizardstate.cpp b/src/gui/newwizard/states/serverurlsetupwizardstate.cpp index 58e07cccc7..3cc658a53f 100644 --- a/src/gui/newwizard/states/serverurlsetupwizardstate.cpp +++ b/src/gui/newwizard/states/serverurlsetupwizardstate.cpp @@ -14,7 +14,6 @@ #include "gui/newwizard/states/serverurlsetupwizardstate.h" -#include "gui/newwizard/jobs/discoverwebfingerservicejobfactory.h" #include "gui/newwizard/jobs/resolveurljobfactory.h" #include "gui/newwizard/pages/serverurlsetupwizardpage.h" #include "libsync/theme.h" @@ -68,23 +67,7 @@ void ServerUrlSetupWizardState::evaluatePage() } _context->accountBuilder().setServerUrl(resolveJob->result().toUrl()); - // FIRST WEBFINGER CALL (unauthenticated): - // This discovers if the server supports WebFinger-based authentication - // and finds the IdP URL. The resource parameter is the server URL itself. - // Looking for: rel="http://openid.net/specs/connect/1.0/issuer" - // See issue #271 for why we perform WebFinger twice. - // Backend WebFinger docs: https://github.com/opencloud-eu/opencloud/blob/main/services/webfinger/README.md - auto *checkWebFingerAuthJob = - Jobs::DiscoverWebFingerServiceJobFactory(_context->accessManager()).startJob(_context->accountBuilder().serverUrl(), this); - connect(checkWebFingerAuthJob, &CoreJob::finished, this, [checkWebFingerAuthJob, this]() { - // in case any kind of error occurs, we assume the WebFinger service is not available - if (!checkWebFingerAuthJob->success()) { - Q_EMIT evaluationSuccessful(); - } else { - _context->accountBuilder().setWebFingerAuthenticationServerUrl(checkWebFingerAuthJob->result().toUrl()); - Q_EMIT evaluationSuccessful(); - } - }); + Q_EMIT evaluationSuccessful(); }); connect( diff --git a/src/libsync/creds/oauth.cpp b/src/libsync/creds/oauth.cpp index 2d931e3b48..736d203b9a 100644 --- a/src/libsync/creds/oauth.cpp +++ b/src/libsync/creds/oauth.cpp @@ -34,6 +34,7 @@ #include #include #include +#include using namespace std::chrono; using namespace std::chrono_literals; @@ -237,6 +238,7 @@ OAuth::OAuth(const QUrl &serverUrl, QNetworkAccessManager *networkAccessManager, , _networkAccessManager(networkAccessManager) , _clientId(Theme::instance()->oauthClientId()) , _clientSecret(Theme::instance()->oauthClientSecret()) + , _scopes(Theme::instance()->openIdConnectScopes()) , _supportedPromtValues(defaultOauthPromtValue()) { } @@ -433,7 +435,7 @@ QNetworkReply *OAuth::postTokenRequest(QUrlQuery &&queryItems) req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded; charset=UTF-8")); req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); - queryItems.addQueryItem(QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes()))); + queryItems.addQueryItem(QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(this->_scopes))); req.setUrl(_tokenEndpoint); return _networkAccessManager->post(req, queryItems.toString(QUrl::FullyEncoded).toUtf8()); } @@ -460,7 +462,7 @@ QUrl OAuth::authorisationLink() const {QStringLiteral("redirect_uri"), QStringLiteral("%1:%2").arg(redirectUrlC(), QString::number(_server.serverPort()))}, {QStringLiteral("code_challenge"), QString::fromLatin1(code_challenge)}, {QStringLiteral("code_challenge_method"), QStringLiteral("S256")}, - {QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes()))}, + {QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(this->_scopes))}, {QStringLiteral("prompt"), QString::fromUtf8(QUrl::toPercentEncoding(toString(_supportedPromtValues)))}, {QStringLiteral("state"), QString::fromUtf8(_state)}, }; @@ -537,68 +539,160 @@ void OAuth::fetchWellKnown() _wellKnownFinished = true; Q_EMIT fetchWellKnownFinished(); } else { - qCDebug(lcOauth) << u"fetching" << wellKnownPathC; + QNetworkRequest webfingerReq; + webfingerReq.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); + webfingerReq.setUrl(Utility::concatUrlPath(_serverUrl, QStringLiteral("/.well-known/webfinger"), + { + {QStringLiteral("resource"), _serverUrl.toString()}, + {QStringLiteral("rel"), QStringLiteral("http://openid.net/specs/connect/1.0/issuer")}, + {QStringLiteral("platform"), QStringLiteral("desktop")}, + })); + webfingerReq.setTransferTimeout(defaultTimeoutMs()); + + auto webfingerReply = _networkAccessManager->get(webfingerReq); + + connect(webfingerReply, &QNetworkReply::finished, this, [webfingerReply, this] { + if (webfingerReply->error() != QNetworkReply::NoError) { + Q_EMIT result(Error); + return; + } - QNetworkRequest req; - req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); - req.setUrl(Utility::concatUrlPath(_serverUrl, wellKnownPathC)); - req.setTransferTimeout(defaultTimeoutMs()); + const QString contentTypeHeader = webfingerReply->header(QNetworkRequest::ContentTypeHeader).toString(); + if (!contentTypeHeader.contains(QStringLiteral("application/json"), Qt::CaseInsensitive)) { + qCWarning(lcOauth) << u"server sent invalid content type:" << contentTypeHeader; + Q_EMIT result(Error); + return; + } - auto reply = _networkAccessManager->get(req); + QJsonParseError error; + const auto doc = QJsonDocument::fromJson(webfingerReply->readAll(), &error); - connect(reply, &QNetworkReply::finished, this, [reply, this] { - _wellKnownFinished = true; - if (reply->error() != QNetworkReply::NoError) { - qCDebug(lcOauth) << u"failed to fetch .well-known reply, error:" << reply->error(); - if (_isRefreshingToken) { - Q_EMIT refreshError(reply->error(), reply->errorString()); - } else { - Q_EMIT result(Error); - } + // empty or invalid response + if (error.error != QJsonParseError::NoError || doc.isNull()) { + qCWarning(lcOauth) << u"could not parse JSON response from server"; + Q_EMIT result(Error); return; } - QJsonParseError err = {}; - QJsonObject data = QJsonDocument::fromJson(reply->readAll(), &err).object(); - if (err.error == QJsonParseError::NoError) { - _authEndpoint = QUrl::fromEncoded(data[QStringLiteral("authorization_endpoint")].toString().toUtf8()); - _tokenEndpoint = QUrl::fromEncoded(data[QStringLiteral("token_endpoint")].toString().toUtf8()); - _registrationEndpoint = QUrl::fromEncoded(data[QStringLiteral("registration_endpoint")].toString().toUtf8()); - - if (_clientSecret.isEmpty()) { - _endpointAuthMethod = TokenEndpointAuthMethods::none; - } else { - const auto authMethods = data.value(QStringLiteral("token_endpoint_auth_methods_supported")).toArray(); - if (authMethods.contains(QStringLiteral("none"))) { - _endpointAuthMethod = TokenEndpointAuthMethods::none; - } else if (authMethods.contains(QStringLiteral("client_secret_post"))) { - _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_post; - } else if (authMethods.contains(QStringLiteral("client_secret_basic"))) { - _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic; + + // make sure the reported subject matches the requested resource + const auto subject = doc.object().value(QStringLiteral("subject")); + if (subject != _serverUrl.toString()) { + qCWarning(lcOauth) << u"reply sent for different subject (server):" << subject; + Q_EMIT result(Error); + return; + } + + // check for an OIDC issuer in the list of links provided (we use the first that matches our conditions) + const auto links = doc.object().value(QStringLiteral("links")).toArray(); + const auto objects = std::views::transform(links, [](const QJsonValueConstRef &object) { return object.toObject(); }); + const auto link = std::ranges::find_if(objects, [](const QJsonObject &linkObject) { + return linkObject.value(QStringLiteral("rel")).toString() == QStringLiteral("http://openid.net/specs/connect/1.0/issuer"); + }); + if (link == objects.end()) { + qCWarning(lcOauth) << u"could not find suitable relation in WebFinger response"; + Q_EMIT result(Error); + return; + } + + auto const issuerUrl = (*link).value(QStringLiteral("href")).toString(); + if (issuerUrl.isNull()) { + qCWarning(lcOauth) << u"could not find href in WebFinger response"; + Q_EMIT result(Error); + return; + } + + const auto properties = doc.object().value(QStringLiteral("properties")).toObject(); + if (const auto clientId = properties.value(QStringLiteral("http://opencloud.eu/ns/oidc/client_id")).toString(); !clientId.isNull()) { + this->_clientId = clientId; + } + if (const auto scopes = properties.value(QStringLiteral("http://opencloud.eu/ns/oidc/scopes")); scopes.isArray()) { + QString scopesJoined; + for (auto element : scopes.toArray()) { + auto scope = element.toString(); + + if (scope.isNull()) { + qCWarning(lcOauth) << u"unexpected non-string scope received from WebFinger, ignoring"; + continue; + } + if (scope.isEmpty()) { + qCWarning(lcOauth) << u"empty scope received from WebFinger, ignoring"; + continue; + } + + if (scopesJoined.isEmpty()) { + scopesJoined = scope; } else { - OC_ASSERT_X( - false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods)))); + scopesJoined.append(QStringLiteral(" ")); + scopesJoined.append(scope); } } - const auto promtValuesSupported = data.value(QStringLiteral("prompt_values_supported")).toArray(); - if (!promtValuesSupported.isEmpty()) { - _supportedPromtValues = PromptValuesSupported::none; - for (const auto &x : promtValuesSupported) { - const auto flag = Utility::stringToEnum(x.toString()); - // only use flags present in Theme::instance()->openIdConnectPrompt() - if (flag & defaultOauthPromtValue()) - _supportedPromtValues |= flag; + this->_scopes = scopesJoined; + } + + auto const oidcWellKnownUrl = Utility::concatUrlPath(QUrl(issuerUrl), wellKnownPathC); + qCDebug(lcOauth) << u"fetching" << oidcWellKnownUrl; + + QNetworkRequest req; + req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); + req.setUrl(oidcWellKnownUrl); + req.setTransferTimeout(defaultTimeoutMs()); + + auto reply = _networkAccessManager->get(req); + + connect(reply, &QNetworkReply::finished, this, [reply, this] { + _wellKnownFinished = true; + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcOauth) << u"failed to fetch .well-known reply, error:" << reply->error(); + if (_isRefreshingToken) { + Q_EMIT refreshError(reply->error(), reply->errorString()); + } else { + Q_EMIT result(Error); } + return; } + QJsonParseError err = {}; + QJsonObject data = QJsonDocument::fromJson(reply->readAll(), &err).object(); + if (err.error == QJsonParseError::NoError) { + _authEndpoint = QUrl::fromEncoded(data[QStringLiteral("authorization_endpoint")].toString().toUtf8()); + _tokenEndpoint = QUrl::fromEncoded(data[QStringLiteral("token_endpoint")].toString().toUtf8()); + _registrationEndpoint = QUrl::fromEncoded(data[QStringLiteral("registration_endpoint")].toString().toUtf8()); + + if (_clientSecret.isEmpty()) { + _endpointAuthMethod = TokenEndpointAuthMethods::none; + } else { + const auto authMethods = data.value(QStringLiteral("token_endpoint_auth_methods_supported")).toArray(); + if (authMethods.contains(QStringLiteral("none"))) { + _endpointAuthMethod = TokenEndpointAuthMethods::none; + } else if (authMethods.contains(QStringLiteral("client_secret_post"))) { + _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_post; + } else if (authMethods.contains(QStringLiteral("client_secret_basic"))) { + _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic; + } else { + OC_ASSERT_X( + false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods)))); + } + } + const auto promtValuesSupported = data.value(QStringLiteral("prompt_values_supported")).toArray(); + if (!promtValuesSupported.isEmpty()) { + _supportedPromtValues = PromptValuesSupported::none; + for (const auto &x : promtValuesSupported) { + const auto flag = Utility::stringToEnum(x.toString()); + // only use flags present in Theme::instance()->openIdConnectPrompt() + if (flag & defaultOauthPromtValue()) + _supportedPromtValues |= flag; + } + } - qCDebug(lcOauth) << u"parsing .well-known reply successful, auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint - << u"and registration endpoint" << _registrationEndpoint; - } else if (err.error == QJsonParseError::IllegalValue) { - qCDebug(lcOauth) << u"failed to parse .well-known reply as JSON, server might not support OIDC"; - } else { - qCDebug(lcOauth) << u"failed to parse .well-known reply, error:" << err.error; - Q_EMIT result(Error); - } - Q_EMIT fetchWellKnownFinished(); + qCDebug(lcOauth) << u"parsing .well-known reply successful, auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint + << u"and registration endpoint" << _registrationEndpoint; + } else if (err.error == QJsonParseError::IllegalValue) { + qCDebug(lcOauth) << u"failed to parse .well-known reply as JSON, server might not support OIDC"; + } else { + qCDebug(lcOauth) << u"failed to parse .well-known reply, error:" << err.error; + Q_EMIT result(Error); + } + Q_EMIT fetchWellKnownFinished(); + }); }); } } diff --git a/src/libsync/creds/oauth.h b/src/libsync/creds/oauth.h index 1a34224a1f..5ee9a8f0a2 100644 --- a/src/libsync/creds/oauth.h +++ b/src/libsync/creds/oauth.h @@ -110,6 +110,7 @@ class OPENCLOUD_SYNC_EXPORT OAuth : public QObject QString _clientId; QString _clientSecret; + QString _scopes; QUrl _registrationEndpoint; diff --git a/test/testconnectionvalidator/testconnectionvalidator.cpp b/test/testconnectionvalidator/testconnectionvalidator.cpp index 068eb9694a..9f97d6913e 100644 --- a/test/testconnectionvalidator/testconnectionvalidator.cpp +++ b/test/testconnectionvalidator/testconnectionvalidator.cpp @@ -75,7 +75,7 @@ private Q_SLOTS: return new FakeErrorReply(op, request, this, 500); } } - return new FakePayloadReply(op, request, TestUtils::getPayloadTemplated(QStringLiteral("status.php.json.in"), values), this); + return new FakePayloadReply(op, request, TestUtils::getPayloadTemplated(QStringLiteral("status.php.json.in"), values), {}, this); } else if (path.endsWith(QLatin1String("capabilities"))) { reachedStage = FailStage::Capabilities; if (failStage == FailStage::Capabilities) { @@ -87,7 +87,7 @@ private Q_SLOTS: return new FakeHangingReply(op, request, this); } } - return new FakePayloadReply(op, request, TestUtils::getPayloadTemplated(QStringLiteral("capabilities.json.in"), values), this); + return new FakePayloadReply(op, request, TestUtils::getPayloadTemplated(QStringLiteral("capabilities.json.in"), values), {}, this); } else if (path.endsWith("graph/v1.0/me"_L1)) { reachedStage = FailStage::UserInfo; if (failStage == FailStage::UserInfo) { @@ -97,7 +97,7 @@ private Q_SLOTS: return new FakeHangingReply(op, request, this); } } - return new FakePayloadReply(op, request, TestUtils::getPayload(QStringLiteral("user.json")), this); + return new FakePayloadReply(op, request, TestUtils::getPayload(QStringLiteral("user.json")), {}, this); } } return nullptr; diff --git a/test/testoauth/testoauth.cpp b/test/testoauth/testoauth.cpp index 6df7ae0998..34846df34a 100644 --- a/test/testoauth/testoauth.cpp +++ b/test/testoauth/testoauth.cpp @@ -134,7 +134,9 @@ class OAuthTestCase : public QObject account->setCredentials(new FakeCredentials{fakeAm}); fakeAm->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { if (req.url().path().endsWith(QLatin1String(".well-known/openid-configuration"))) { - return this->wellKnownReply(op, req); + return this->wellKnownOpenidConfigurationReply(op, req); + } else if (req.url().path().endsWith(QLatin1String(".well-known/webfinger"))) { + return this->wellKnownWebfingerReply(op, req); } else if (req.url().path().endsWith(QLatin1String("status.php"))) { return this->statusPhpReply(op, req); } else if (req.url().path().endsWith(QLatin1String("clients-registrations"))) { @@ -224,7 +226,7 @@ class OAuthTestCase : public QObject return new FakePostReply(op, req, std::move(payload), fakeAm); } - virtual QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) + virtual QNetworkReply *wellKnownOpenidConfigurationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) { OC_ASSERT(op == QNetworkAccessManager::GetOperation); QJsonDocument jsondata(QJsonObject{ @@ -233,7 +235,25 @@ class OAuthTestCase : public QObject {QStringLiteral("token_endpoint"), Utility::concatUrlPath(sOAuthTestServer, QStringLiteral("token_endpoint")).toString()}, {QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_post")}}, }); - return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm); + return new FakePayloadReply(op, req, jsondata.toJson(), {}, fakeAm); + } + + virtual QNetworkReply *wellKnownWebfingerReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) + { + OC_ASSERT(op == QNetworkAccessManager::GetOperation); + QJsonDocument jsondata(QJsonObject{ + {QStringLiteral("subject"), sOAuthTestServer.toString() }, + {QStringLiteral("links"), QJsonArray{ + QJsonObject{ + {QStringLiteral("rel"), QStringLiteral("http://openid.net/specs/connect/1.0/issuer") }, + {QStringLiteral("href"), QStringLiteral("oauthtest://openidserver") }, + } + }}, + }); + QHttpHeaders headers; + headers.append(QHttpHeaders::WellKnownHeader::ContentType, QStringLiteral("application/json")); + + return new FakePayloadReply(op, req, jsondata.toJson(), headers, fakeAm); } virtual QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) @@ -470,7 +490,7 @@ private Q_SLOTS: { Test() { } - QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override + QNetworkReply *wellKnownOpenidConfigurationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override { OC_ASSERT(op == QNetworkAccessManager::GetOperation); const QJsonDocument jsondata(QJsonObject{ @@ -480,7 +500,7 @@ private Q_SLOTS: {QStringLiteral("registration_endpoint"), QStringLiteral("%1/clients-registrations").arg(localHost)}, {QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_basic")}}, }); - return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm); + return new FakePayloadReply(op, req, jsondata.toJson(), {}, fakeAm); } void openBrowserHook(const QUrl &url) override @@ -513,7 +533,7 @@ private Q_SLOTS: { Test() { } - QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override + QNetworkReply *wellKnownOpenidConfigurationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override { OC_ASSERT(op == QNetworkAccessManager::GetOperation); const QJsonDocument jsondata(QJsonObject{ @@ -524,7 +544,7 @@ private Q_SLOTS: {QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_basic"), QStringLiteral("client_secret_post")}}, }); - return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm); + return new FakePayloadReply(op, req, jsondata.toJson(), {}, fakeAm); } void openBrowserHook(const QUrl &url) override @@ -548,7 +568,7 @@ private Q_SLOTS: QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override { - return new FakePayloadReply(op, request, {}, fakeAm); + return new FakePayloadReply(op, request, {}, {}, fakeAm); } } test; @@ -563,7 +583,7 @@ private Q_SLOTS: { Test() { _expectedClientId = QStringLiteral("3e4ea0f3-59ea-434a-92f2-b0d3b54443e9"); } - QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override + QNetworkReply *wellKnownOpenidConfigurationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override { OC_ASSERT(op == QNetworkAccessManager::GetOperation); const QJsonDocument jsondata(QJsonObject{ @@ -574,7 +594,7 @@ private Q_SLOTS: {QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_basic")}}, }); - return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm); + return new FakePayloadReply(op, req, jsondata.toJson(), {}, fakeAm); } void openBrowserHook(const QUrl &url) override @@ -614,7 +634,7 @@ private Q_SLOTS: "v1giSvpnKw1hTtBYZaqdp3JqnZ5mvCKYhQDKkT7x8Us\",\"backchannel_logout_session_required\":false,\"require_pushed_authorization_requests\":" "false}")); - auto *out = new FakePayloadReply(op, request, payload, fakeAm); + auto *out = new FakePayloadReply(op, request, payload, {}, fakeAm); out->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); return out; } @@ -644,7 +664,7 @@ private Q_SLOTS: QString _expectedClientSecret = QStringLiteral("rmoEXFc1Z5tGTApxanBW7STlWODqRTYx"); Test() { _expectedClientId = QStringLiteral("3e4ea0f3-59ea-434a-92f2-b0d3b54443e9"); } - QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override + QNetworkReply *wellKnownOpenidConfigurationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override { OC_ASSERT(op == QNetworkAccessManager::GetOperation); const QJsonDocument jsondata(QJsonObject{ @@ -655,7 +675,7 @@ private Q_SLOTS: // this test explicitly check for the client secret in the post body {QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_post")}}, }); - return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm); + return new FakePayloadReply(op, req, jsondata.toJson(), {}, fakeAm); } void openBrowserHook(const QUrl &) override { Q_UNREACHABLE(); } @@ -679,7 +699,7 @@ private Q_SLOTS: QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override { - auto *out = new FakePayloadReply(op, request, TestUtils::getPayload("testDynamicTokenRefresh/clientRegistrationReply.json"), fakeAm); + auto *out = new FakePayloadReply(op, request, TestUtils::getPayload("testDynamicTokenRefresh/clientRegistrationReply.json"), {}, fakeAm); out->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); return out; } @@ -707,7 +727,7 @@ private Q_SLOTS: { Test() { } - QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override + QNetworkReply *wellKnownOpenidConfigurationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override { OC_ASSERT(op == QNetworkAccessManager::GetOperation); const QJsonDocument jsondata(QJsonObject{ @@ -718,7 +738,7 @@ private Q_SLOTS: // this test explicitly check for the client secret in the post body {QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_post")}}, }); - return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm); + return new FakePayloadReply(op, req, jsondata.toJson(), {}, fakeAm); } void openBrowserHook(const QUrl &) override { Q_UNREACHABLE(); } @@ -742,7 +762,7 @@ private Q_SLOTS: QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override { - return new FakePayloadReply(op, request, {}, fakeAm); + return new FakePayloadReply(op, request, {}, {}, fakeAm); } virtual void test() override diff --git a/test/testutils/syncenginetestutils.cpp b/test/testutils/syncenginetestutils.cpp index d700898e1b..5568445702 100644 --- a/test/testutils/syncenginetestutils.cpp +++ b/test/testutils/syncenginetestutils.cpp @@ -685,9 +685,10 @@ qint64 FakeGetReply::readData(char *data, qint64 maxlen) return len; } -FakePayloadReply::FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, QObject *parent) +FakePayloadReply::FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, const QHttpHeaders &headers, QObject *parent) : FakeReply { parent } , _body(body) + , _headers(headers) { setRequest(request); setUrl(request.url()); @@ -701,6 +702,7 @@ void FakePayloadReply::respond() { if (error() == QNetworkReply::NoError) { setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); + setHeaders(_headers); Q_EMIT metaDataChanged(); Q_EMIT readyRead(); checkedFinished(); diff --git a/test/testutils/syncenginetestutils.h b/test/testutils/syncenginetestutils.h index 55293f03ab..3ae0ffdfbb 100644 --- a/test/testutils/syncenginetestutils.h +++ b/test/testutils/syncenginetestutils.h @@ -413,13 +413,14 @@ class FakePayloadReply : public FakeReply Q_OBJECT public: FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, - const QByteArray &body, QObject *parent); + const QByteArray &body, const QHttpHeaders &headers, QObject *parent); virtual void respond(); qint64 readData(char *buf, qint64 max) override; qint64 bytesAvailable() const override; QByteArray _body; + QHttpHeaders _headers; };