diff --git a/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp b/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp index 0b05e6858f..169072a473 100644 --- a/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp +++ b/src/gui/newwizard/jobs/discoverwebfingerservicejobfactory.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace OCC::Wizard::Jobs { @@ -73,20 +74,67 @@ CoreJob *DiscoverWebFingerServiceJobFactory::startJob(const QUrl &url, QObject * return; } - // check for an OIDC issuer in the list of links provided (we use the first that matches our conditions) + // Check for an OIDC issuer in the list of links provided. + // We prioritize a desktop-specific issuer over the generic one to support + // identity providers that require separate OIDC client configurations per application type. + // See: https://github.com/opencloud-eu/desktop/issues/246 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; + + // Helper struct to hold issuer info including optional client_id from properties + struct IssuerInfo { + QString href; + QString clientId; + }; + + // Helper lambda to find an OIDC issuer with a specific relation type + // Also extracts the client_id from properties if present + auto findIssuerByRelation = [&links](const QString &relation) -> IssuerInfo { + for (const auto &link : links) { + const auto linkObject = link.toObject(); + if (linkObject.value(QStringLiteral("rel")).toString() == relation) { + IssuerInfo info; + info.href = linkObject.value(QStringLiteral("href")).toString(); + // Check for client_id in properties + const auto properties = linkObject.value(QStringLiteral("properties")).toObject(); + const QString clientIdProperty = QStringLiteral("http://openid.net/specs/connect/1.0/client_id"); + if (properties.contains(clientIdProperty)) { + info.clientId = properties.value(clientIdProperty).toString(); + } + return info; + } + } + return {}; + }; + + // First, try desktop-specific OIDC issuer + const QString desktopIssuerRel = QStringLiteral("http://openid.net/specs/connect/1.0/issuer/desktop"); + IssuerInfo issuerInfo = findIssuerByRelation(desktopIssuerRel); + if (!issuerInfo.href.isEmpty()) { + qCInfo(lcDiscoverWebFingerService) << u"using desktop-specific OIDC issuer:" << issuerInfo.href; + if (!issuerInfo.clientId.isEmpty()) { + qCInfo(lcDiscoverWebFingerService) << u"using desktop-specific client_id:" << issuerInfo.clientId; } + // Return both issuer URL and client_id as a map + QVariantMap result; + result[QStringLiteral("issuer")] = QUrl::fromUserInput(issuerInfo.href); + result[QStringLiteral("clientId")] = issuerInfo.clientId; + setJobResult(job, result); + return; + } + + // Fall back to generic OIDC issuer + const QString genericIssuerRel = QStringLiteral("http://openid.net/specs/connect/1.0/issuer"); + issuerInfo = findIssuerByRelation(genericIssuerRel); + if (!issuerInfo.href.isEmpty()) { + qCDebug(lcDiscoverWebFingerService) << u"using generic OIDC issuer:" << issuerInfo.href; + QVariantMap result; + result[QStringLiteral("issuer")] = QUrl::fromUserInput(issuerInfo.href); + result[QStringLiteral("clientId")] = QString(); // No client_id for generic issuer + setJobResult(job, result); + return; } - qCWarning(lcDiscoverWebFingerService) << u"could not find suitable relation in WebFinger response"; + qCWarning(lcDiscoverWebFingerService) << u"could not find OIDC issuer relation in WebFinger response"; setInvalidReplyError(); }); diff --git a/src/gui/newwizard/setupwizardaccountbuilder.cpp b/src/gui/newwizard/setupwizardaccountbuilder.cpp index 42ffc27c12..ab27c2d204 100644 --- a/src/gui/newwizard/setupwizardaccountbuilder.cpp +++ b/src/gui/newwizard/setupwizardaccountbuilder.cpp @@ -90,7 +90,7 @@ AccountPtr SetupWizardAccountBuilder::build() const // TODO: perhaps _authenticationStrategy->setUpAccountPtr(...) would be more elegant? no need for getters then newAccountPtr->setCredentials(_authenticationStrategy->makeCreds()); newAccountPtr->credentials()->persist(); - OAuth::persist(newAccountPtr, _authenticationStrategy->dynamicRegistrationData(), _authenticationStrategy->idToken()); + OAuth::persist(newAccountPtr, _authenticationStrategy->dynamicRegistrationData(), _authenticationStrategy->idToken(), _webFingerDesktopClientId); newAccountPtr->setDavDisplayName(_displayName); @@ -166,6 +166,16 @@ QUrl SetupWizardAccountBuilder::webFingerAuthenticationServerUrl() const return _webFingerAuthenticationServerUrl; } +void SetupWizardAccountBuilder::setWebFingerDesktopClientId(const QString &clientId) +{ + _webFingerDesktopClientId = clientId; +} + +QString SetupWizardAccountBuilder::webFingerDesktopClientId() const +{ + return _webFingerDesktopClientId; +} + void SetupWizardAccountBuilder::setWebFingerInstances(const QVector &instancesList) { _webFingerInstances = instancesList; diff --git a/src/gui/newwizard/setupwizardaccountbuilder.h b/src/gui/newwizard/setupwizardaccountbuilder.h index 589161d8c8..84c7af0682 100644 --- a/src/gui/newwizard/setupwizardaccountbuilder.h +++ b/src/gui/newwizard/setupwizardaccountbuilder.h @@ -98,6 +98,9 @@ class SetupWizardAccountBuilder void setWebFingerAuthenticationServerUrl(const QUrl &url); QUrl webFingerAuthenticationServerUrl() const; + void setWebFingerDesktopClientId(const QString &clientId); + QString webFingerDesktopClientId() const; + void setWebFingerInstances(const QVector &instancesList); QVector webFingerInstances() const; @@ -108,6 +111,7 @@ class SetupWizardAccountBuilder QUrl _serverUrl; QUrl _webFingerAuthenticationServerUrl; + QString _webFingerDesktopClientId; QVector _webFingerInstances; QUrl _webFingerSelectedInstance; diff --git a/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp b/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp index c033ee55d7..8018bf85e9 100644 --- a/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp +++ b/src/gui/newwizard/states/oauthcredentialssetupwizardstate.cpp @@ -30,6 +30,14 @@ OAuthCredentialsSetupWizardState::OAuthCredentialsSetupWizardState(SetupWizardCo }(); auto oAuth = new OAuth(authServerUrl, _context->accessManager(), {}, this); + + // Use the desktop-specific client_id if provided by webfinger + // See: https://github.com/opencloud-eu/desktop/issues/246 + const QString desktopClientId = _context->accountBuilder().webFingerDesktopClientId(); + if (!desktopClientId.isEmpty()) { + oAuth->setClientId(desktopClientId); + } + _page = new OAuthCredentialsSetupWizardPage(oAuth, authServerUrl); diff --git a/src/gui/newwizard/states/serverurlsetupwizardstate.cpp b/src/gui/newwizard/states/serverurlsetupwizardstate.cpp index 58e07cccc7..31e4ec616f 100644 --- a/src/gui/newwizard/states/serverurlsetupwizardstate.cpp +++ b/src/gui/newwizard/states/serverurlsetupwizardstate.cpp @@ -81,7 +81,13 @@ void ServerUrlSetupWizardState::evaluatePage() if (!checkWebFingerAuthJob->success()) { Q_EMIT evaluationSuccessful(); } else { - _context->accountBuilder().setWebFingerAuthenticationServerUrl(checkWebFingerAuthJob->result().toUrl()); + // Result is now a QVariantMap with "issuer" and "clientId" keys + const auto resultMap = checkWebFingerAuthJob->result().toMap(); + _context->accountBuilder().setWebFingerAuthenticationServerUrl(resultMap.value(QStringLiteral("issuer")).toUrl()); + const QString clientId = resultMap.value(QStringLiteral("clientId")).toString(); + if (!clientId.isEmpty()) { + _context->accountBuilder().setWebFingerDesktopClientId(clientId); + } Q_EMIT evaluationSuccessful(); } }); diff --git a/src/libsync/creds/oauth.cpp b/src/libsync/creds/oauth.cpp index a22c92726b..340ca07952 100644 --- a/src/libsync/creds/oauth.cpp +++ b/src/libsync/creds/oauth.cpp @@ -101,6 +101,11 @@ QString idTokenC() return QStringLiteral("oauth/id_token"); } +QString desktopClientIdC() +{ + return QStringLiteral("oauth/desktopClientId"); +} + QVariant getRequiredField(const QVariantMap &json, const QString &s, QString *error) { const auto out = json.constFind(s); @@ -481,7 +486,12 @@ QString OAuth::clientSecret() const return _clientSecret; } -void OAuth::persist(const OCC::AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken) +void OAuth::setClientId(const QString &clientId) +{ + _clientId = clientId; +} + +void OAuth::persist(const OCC::AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken, const QString &desktopClientId) { if (!dynamicRegistrationData.isEmpty()) { accountPtr->credentialManager()->set(dynamicRegistrationDataC(), dynamicRegistrationData); @@ -493,6 +503,13 @@ void OAuth::persist(const OCC::AccountPtr &accountPtr, const QVariantMap &dynami } else { accountPtr->credentialManager()->clear(idTokenC()); } + // Store desktop-specific client_id from webfinger if provided + // See: https://github.com/opencloud-eu/desktop/issues/246 + if (!desktopClientId.isEmpty()) { + accountPtr->credentialManager()->set(desktopClientIdC(), desktopClientId); + } else { + accountPtr->credentialManager()->clear(desktopClientIdC()); + } } void OAuth::updateDynamicRegistration() @@ -708,7 +725,20 @@ void AccountBasedOAuth::restore() logCredentialsJobResult(credentialsJob); _dynamicRegistrationData = credentialsJob->data().value(); - Q_EMIT restored(QPrivateSignal()); + + // Also restore the desktop-specific client_id if stored + // See: https://github.com/opencloud-eu/desktop/issues/246 + auto clientIdJob = _account->credentialManager()->get(desktopClientIdC()); + connect(clientIdJob, &CredentialJob::finished, this, [this, clientIdJob] { + if (clientIdJob->error() == QKeychain::NoError) { + const QString storedClientId = clientIdJob->data().toString(); + if (!storedClientId.isEmpty()) { + qCInfo(lcOauth) << u"restored desktop-specific client_id"; + setClientId(storedClientId); + } + } + Q_EMIT restored(QPrivateSignal()); + }); }); }); } diff --git a/src/libsync/creds/oauth.h b/src/libsync/creds/oauth.h index 1a34224a1f..4eeba0f15a 100644 --- a/src/libsync/creds/oauth.h +++ b/src/libsync/creds/oauth.h @@ -79,7 +79,14 @@ class OPENCLOUD_SYNC_EXPORT OAuth : public QObject QString clientId() const; QString clientSecret() const; - static void persist(const AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken); + /** + * Set a custom client_id to use instead of the theme default. + * This is used when the server provides a desktop-specific client_id via webfinger. + * Must be called before startAuthentication(). + */ + void setClientId(const QString &clientId); + + static void persist(const AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken, const QString &desktopClientId = {}); Q_SIGNALS: /**