From 4386b1d0a693b33e39d517c18cc0487f18aca2e9 Mon Sep 17 00:00:00 2001 From: kaivol Date: Thu, 1 Jan 2026 23:46:39 +0100 Subject: [PATCH 1/2] remove unnecessary webfinger query --- src/gui/newwizard/CMakeLists.txt | 3 - .../discoverwebfingerservicejobfactory.cpp | 101 ------------------ .../jobs/discoverwebfingerservicejobfactory.h | 31 ------ .../newwizard/setupwizardaccountbuilder.cpp | 10 -- src/gui/newwizard/setupwizardaccountbuilder.h | 4 - .../oauthcredentialssetupwizardstate.cpp | 39 +++---- .../states/serverurlsetupwizardstate.cpp | 19 +--- 7 files changed, 14 insertions(+), 193 deletions(-) delete mode 100644 src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp delete mode 100644 src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.h 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( From 31745c49f42b56dff1e5ca2823561a0b33fa31d3 Mon Sep 17 00:00:00 2001 From: kaivol Date: Sun, 4 Jan 2026 19:42:30 +0100 Subject: [PATCH 2/2] query WebFinger for the current OIDC issuer everytime we fetch the `/.well-known/openid-configuration` --- src/libsync/creds/oauth.cpp | 165 ++++++++++++++++++++++++------------ 1 file changed, 113 insertions(+), 52 deletions(-) diff --git a/src/libsync/creds/oauth.cpp b/src/libsync/creds/oauth.cpp index a22c92726b..eabeba6fe0 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; @@ -537,68 +538,128 @@ 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()}})); + webfingerReq.setTransferTimeout(defaultTimeoutMs()); - QNetworkRequest req; - req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); - req.setUrl(Utility::concatUrlPath(_serverUrl, wellKnownPathC)); - req.setTransferTimeout(defaultTimeoutMs()); + auto webfingerReply = _networkAccessManager->get(webfingerReq); - auto reply = _networkAccessManager->get(req); + connect(webfingerReply, &QNetworkReply::finished, this, [webfingerReply, this] { + if (webfingerReply->error() != QNetworkReply::NoError) { + Q_EMIT result(Error); + return; + } - 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); - } + 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; } - 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; + + QJsonParseError error; + const auto doc = QJsonDocument::fromJson(webfingerReply->readAll(), &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; + } + + // 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; + } + + 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 { - OC_ASSERT_X( - false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods)))); + Q_EMIT result(Error); } + return; } - 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; + 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(); + }); }); } }