Skip to content

Commit 31745c4

Browse files
committed
query WebFinger for the current OIDC issuer everytime we fetch the /.well-known/openid-configuration
1 parent 4386b1d commit 31745c4

File tree

1 file changed

+113
-52
lines changed

1 file changed

+113
-52
lines changed

src/libsync/creds/oauth.cpp

Lines changed: 113 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <QNetworkReply>
3535
#include <QPixmap>
3636
#include <QRandomGenerator>
37+
#include <ranges>
3738

3839
using namespace std::chrono;
3940
using namespace std::chrono_literals;
@@ -537,68 +538,128 @@ void OAuth::fetchWellKnown()
537538
_wellKnownFinished = true;
538539
Q_EMIT fetchWellKnownFinished();
539540
} 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());
541546

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);
546548

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+
}
548554

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);
558559
return;
559560
}
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());
577615
} 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);
580617
}
618+
return;
581619
}
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+
}
590651
}
591-
}
592652

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+
});
602663
});
603664
}
604665
}

0 commit comments

Comments
 (0)