From 9089cf63ea6e16d5b18fada71959dfe4c5d43c55 Mon Sep 17 00:00:00 2001 From: pat-s Date: Sat, 20 Dec 2025 20:42:41 +0100 Subject: [PATCH 1/4] feat(webfinger): add desktop-specific OIDC issuer support --- services/webfinger/pkg/command/server.go | 16 +++++++++++ services/webfinger/pkg/config/config.go | 1 + .../pkg/relations/openid_discovery.go | 27 ++++++++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go index fb93d0868d..50e186a6cd 100644 --- a/services/webfinger/pkg/command/server.go +++ b/services/webfinger/pkg/command/server.go @@ -121,6 +121,11 @@ func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvid switch relationURI { case relations.OpenIDConnectRel: rels[relationURI] = relations.OpenIDDiscovery(cfg.IDP) + case relations.OpenIDConnectDesktopRel: + // Handled below - can also be auto-enabled via DesktopIDP config + if cfg.DesktopIDP != "" { + rels[relationURI] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP) + } case relations.OpenCloudInstanceRel: var err error rels[relationURI], err = relations.OpenCloudInstance(cfg.Instances, cfg.OpenCloudURL) @@ -131,5 +136,16 @@ func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvid return nil, fmt.Errorf("unknown relation '%s'", relationURI) } } + + // Auto-enable desktop OIDC issuer when DesktopIDP is configured, + // even if not explicitly listed in Relations. This provides a simpler + // configuration experience - just set WEBFINGER_OIDC_ISSUER_DESKTOP. + // See: https://github.com/opencloud-eu/desktop/issues/246 + if cfg.DesktopIDP != "" { + if _, exists := rels[relations.OpenIDConnectDesktopRel]; !exists { + rels[relations.OpenIDConnectDesktopRel] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP) + } + } + return rels, nil } diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 7055a97ee7..865758dc18 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -20,6 +20,7 @@ type Config struct { Instances []Instance `yaml:"instances"` Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A list of relation URIs or registered relation types to add to webfinger responses. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation." introductionVersion:"1.0.0"` + DesktopIDP string `yaml:"desktop_idp" env:"WEBFINGER_OIDC_ISSUER_DESKTOP" desc:"The identity provider href for desktop clients. When set, desktop clients will use this issuer instead of the default IDP. This allows configuring separate OIDC clients for web and desktop applications." introductionVersion:"%%NEXT%%"` OpenCloudURL string `yaml:"opencloud_url" env:"OC_URL;WEBFINGER_OPENCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the legacy OpenCloud server instance relation (not to be confused with the product OpenCloud Server). It defaults to the OC_URL but can be overridden to support some reverse proxy corner cases. To shard the deployment, multiple instances can be configured in the configuration file." introductionVersion:"1.0.0"` Insecure bool `yaml:"insecure" env:"OC_INSECURE;WEBFINGER_INSECURE" desc:"Allow insecure connections to the WEBFINGER service." introductionVersion:"1.0.0"` diff --git a/services/webfinger/pkg/relations/openid_discovery.go b/services/webfinger/pkg/relations/openid_discovery.go index 196357ee9c..69eab052a9 100644 --- a/services/webfinger/pkg/relations/openid_discovery.go +++ b/services/webfinger/pkg/relations/openid_discovery.go @@ -8,7 +8,8 @@ import ( ) const ( - OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" + OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" + OpenIDConnectDesktopRel = "http://openid.net/specs/connect/1.0/issuer/desktop" ) type openIDDiscovery struct { @@ -31,3 +32,27 @@ func (l *openIDDiscovery) Add(_ context.Context, jrd *webfinger.JSONResourceDesc Href: l.Href, }) } + +type openIDDiscoveryDesktop struct { + Href string +} + +// OpenIDDiscoveryDesktop adds the OpenID Connect issuer relation for desktop clients. +// This allows identity providers that require separate OIDC clients per application type +// (like Authentik, Kanidm, Zitadel) to provide a distinct issuer URL for desktop clients. +// See: https://github.com/opencloud-eu/desktop/issues/246 +func OpenIDDiscoveryDesktop(href string) service.RelationProvider { + return &openIDDiscoveryDesktop{ + Href: href, + } +} + +func (l *openIDDiscoveryDesktop) Add(_ context.Context, jrd *webfinger.JSONResourceDescriptor) { + if jrd == nil { + jrd = &webfinger.JSONResourceDescriptor{} + } + jrd.Links = append(jrd.Links, webfinger.Link{ + Rel: OpenIDConnectDesktopRel, + Href: l.Href, + }) +} From da084e3289acd91667a60109b1e315069dfd937c Mon Sep 17 00:00:00 2001 From: pat-s Date: Sat, 20 Dec 2025 23:45:47 +0100 Subject: [PATCH 2/4] feat(webfinger): support desktop and mobile specific OIDC client_id --- services/webfinger/pkg/command/server.go | 16 +++++- services/webfinger/pkg/config/config.go | 5 +- .../pkg/relations/openid_discovery.go | 56 +++++++++++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go index 50e186a6cd..77a9eb6287 100644 --- a/services/webfinger/pkg/command/server.go +++ b/services/webfinger/pkg/command/server.go @@ -124,7 +124,12 @@ func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvid case relations.OpenIDConnectDesktopRel: // Handled below - can also be auto-enabled via DesktopIDP config if cfg.DesktopIDP != "" { - rels[relationURI] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP) + rels[relationURI] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP, cfg.DesktopClientID) + } + case relations.OpenIDConnectMobileRel: + // Handled below - can also be auto-enabled via MobileIDP config + if cfg.MobileIDP != "" { + rels[relationURI] = relations.OpenIDDiscoveryMobile(cfg.MobileIDP, cfg.MobileClientID) } case relations.OpenCloudInstanceRel: var err error @@ -143,7 +148,14 @@ func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvid // See: https://github.com/opencloud-eu/desktop/issues/246 if cfg.DesktopIDP != "" { if _, exists := rels[relations.OpenIDConnectDesktopRel]; !exists { - rels[relations.OpenIDConnectDesktopRel] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP) + rels[relations.OpenIDConnectDesktopRel] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP, cfg.DesktopClientID) + } + } + + // Auto-enable mobile OIDC issuer when MobileIDP is configured + if cfg.MobileIDP != "" { + if _, exists := rels[relations.OpenIDConnectMobileRel]; !exists { + rels[relations.OpenIDConnectMobileRel] = relations.OpenIDDiscoveryMobile(cfg.MobileIDP, cfg.MobileClientID) } } diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 865758dc18..bb1d48ac7b 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -20,7 +20,10 @@ type Config struct { Instances []Instance `yaml:"instances"` Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A list of relation URIs or registered relation types to add to webfinger responses. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation." introductionVersion:"1.0.0"` - DesktopIDP string `yaml:"desktop_idp" env:"WEBFINGER_OIDC_ISSUER_DESKTOP" desc:"The identity provider href for desktop clients. When set, desktop clients will use this issuer instead of the default IDP. This allows configuring separate OIDC clients for web and desktop applications." introductionVersion:"%%NEXT%%"` + DesktopIDP string `yaml:"desktop_idp" env:"WEBFINGER_OIDC_ISSUER_DESKTOP" desc:"The identity provider href for desktop clients. When set, desktop clients will use this issuer instead of the default IDP. This allows configuring separate OIDC clients for web and desktop applications." introductionVersion:"%%NEXT%%"` + DesktopClientID string `yaml:"desktop_client_id" env:"WEBFINGER_OIDC_CLIENT_ID_DESKTOP" desc:"The OIDC client ID for desktop clients. When set along with WEBFINGER_OIDC_ISSUER_DESKTOP, this client ID will be provided to desktop clients via webfinger properties." introductionVersion:"%%NEXT%%"` + MobileIDP string `yaml:"mobile_idp" env:"WEBFINGER_OIDC_ISSUER_MOBILE" desc:"The identity provider href for mobile clients. When set, mobile clients will use this issuer instead of the default IDP. This allows configuring separate OIDC clients for web and mobile applications." introductionVersion:"%%NEXT%%"` + MobileClientID string `yaml:"mobile_client_id" env:"WEBFINGER_OIDC_CLIENT_ID_MOBILE" desc:"The OIDC client ID for mobile clients. When set along with WEBFINGER_OIDC_ISSUER_MOBILE, this client ID will be provided to mobile clients via webfinger properties." introductionVersion:"%%NEXT%%"` OpenCloudURL string `yaml:"opencloud_url" env:"OC_URL;WEBFINGER_OPENCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the legacy OpenCloud server instance relation (not to be confused with the product OpenCloud Server). It defaults to the OC_URL but can be overridden to support some reverse proxy corner cases. To shard the deployment, multiple instances can be configured in the configuration file." introductionVersion:"1.0.0"` Insecure bool `yaml:"insecure" env:"OC_INSECURE;WEBFINGER_INSECURE" desc:"Allow insecure connections to the WEBFINGER service." introductionVersion:"1.0.0"` diff --git a/services/webfinger/pkg/relations/openid_discovery.go b/services/webfinger/pkg/relations/openid_discovery.go index 69eab052a9..ea63dae336 100644 --- a/services/webfinger/pkg/relations/openid_discovery.go +++ b/services/webfinger/pkg/relations/openid_discovery.go @@ -10,6 +10,7 @@ import ( const ( OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" OpenIDConnectDesktopRel = "http://openid.net/specs/connect/1.0/issuer/desktop" + OpenIDConnectMobileRel = "http://openid.net/specs/connect/1.0/issuer/mobile" ) type openIDDiscovery struct { @@ -33,17 +34,23 @@ func (l *openIDDiscovery) Add(_ context.Context, jrd *webfinger.JSONResourceDesc }) } +// ClientIDProperty is the property URI for the OIDC client ID +const ClientIDProperty = "http://openid.net/specs/connect/1.0/client_id" + type openIDDiscoveryDesktop struct { - Href string + Href string + ClientID string } // OpenIDDiscoveryDesktop adds the OpenID Connect issuer relation for desktop clients. // This allows identity providers that require separate OIDC clients per application type // (like Authentik, Kanidm, Zitadel) to provide a distinct issuer URL for desktop clients. +// If clientID is provided, it will be included as a property in the link. // See: https://github.com/opencloud-eu/desktop/issues/246 -func OpenIDDiscoveryDesktop(href string) service.RelationProvider { +func OpenIDDiscoveryDesktop(href string, clientID string) service.RelationProvider { return &openIDDiscoveryDesktop{ - Href: href, + Href: href, + ClientID: clientID, } } @@ -51,8 +58,47 @@ func (l *openIDDiscoveryDesktop) Add(_ context.Context, jrd *webfinger.JSONResou if jrd == nil { jrd = &webfinger.JSONResourceDescriptor{} } - jrd.Links = append(jrd.Links, webfinger.Link{ + link := webfinger.Link{ Rel: OpenIDConnectDesktopRel, Href: l.Href, - }) + } + if l.ClientID != "" { + link.Properties = map[string]string{ + ClientIDProperty: l.ClientID, + } + } + jrd.Links = append(jrd.Links, link) +} + +type openIDDiscoveryMobile struct { + Href string + ClientID string +} + +// OpenIDDiscoveryMobile adds the OpenID Connect issuer relation for mobile clients. +// This allows identity providers that require separate OIDC clients per application type +// (like Authentik, Kanidm, Zitadel) to provide a distinct issuer URL for mobile clients. +// If clientID is provided, it will be included as a property in the link. +// See: https://github.com/opencloud-eu/desktop/issues/246 +func OpenIDDiscoveryMobile(href string, clientID string) service.RelationProvider { + return &openIDDiscoveryMobile{ + Href: href, + ClientID: clientID, + } +} + +func (l *openIDDiscoveryMobile) Add(_ context.Context, jrd *webfinger.JSONResourceDescriptor) { + if jrd == nil { + jrd = &webfinger.JSONResourceDescriptor{} + } + link := webfinger.Link{ + Rel: OpenIDConnectMobileRel, + Href: l.Href, + } + if l.ClientID != "" { + link.Properties = map[string]string{ + ClientIDProperty: l.ClientID, + } + } + jrd.Links = append(jrd.Links, link) } From f7e88217aecbcdb64f4673c6d9e7d0c43f4b1f2d Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 29 Jan 2026 13:43:11 +0100 Subject: [PATCH 3/4] chore(webfinger): Simplify weird Query parameter extraction loop --- services/webfinger/pkg/server/http/server.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index 239ca5e67a..0f01583c88 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -125,12 +125,7 @@ func WebfingerHandler(service svc.Service) func(w http.ResponseWriter, r *http.R return } - rels := make([]string, 0) - for k, v := range r.URL.Query() { - if k == "rel" { - rels = append(rels, v...) - } - } + rels := r.URL.Query()["rel"] jrd, err := service.Webfinger(ctx, queryTarget, rels) if errors.Is(err, serviceErrors.ErrNotFound) { From 6615f1e1dd99f2d1024f530c295ee5ef6c73d757 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 29 Jan 2026 16:31:23 +0100 Subject: [PATCH 4/4] feat(webfinger): use webfinger properties instead new relations This works the previous commits so that clients can add an addtional 'platform' query parameter to the webfinger request that can be used to query the oidc client id and list of scopes that the clients need to use when connecting to the IDP. This also removes the non-standard issuer relatation introduced in a previous commit as we can just introduce new relations in the http://openid.net name space. For IDP like Authentik that create a separate issuer url per Client (Application in Authentik's terms) it is suggested to just configure as single Client and use that id for all platforms (i.e. setting 'WEBFINGER_ANDROID_OIDC_CLIENT_ID', 'WEBFINGER_DESKTOP_OIDC_CLIENT_ID', 'WEBFINGER_IOS_OIDC_CLIENT_ID' and 'WEBFINGER_WEB_OIDC_CLIENT_ID' to same value. Related: #2088 Related: https://github.com/opencloud-eu/desktop/issues/246 --- services/webfinger/pkg/command/server.go | 29 +----- services/webfinger/pkg/config/config.go | 29 ++++-- .../pkg/config/defaults/defaultconfig.go | 36 +++++++- ...loud_instance.go => opencloud_instance.go} | 2 +- ...nce_test.go => opencloud_instance_test.go} | 2 +- .../pkg/relations/openid_discovery.go | 88 ++++--------------- .../pkg/relations/openid_discovery_test.go | 31 ++++++- services/webfinger/pkg/server/http/server.go | 4 +- .../webfinger/pkg/service/v0/instrument.go | 4 +- services/webfinger/pkg/service/v0/logging.go | 4 +- services/webfinger/pkg/service/v0/service.go | 10 +-- services/webfinger/pkg/service/v0/tracing.go | 4 +- services/webfinger/pkg/webfinger/webfinger.go | 2 +- 13 files changed, 116 insertions(+), 129 deletions(-) rename services/webfinger/pkg/relations/{owncloud_instance.go => opencloud_instance.go} (95%) rename services/webfinger/pkg/relations/{owncloud_instance_test.go => opencloud_instance_test.go} (98%) diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go index 77a9eb6287..49fd8fdbac 100644 --- a/services/webfinger/pkg/command/server.go +++ b/services/webfinger/pkg/command/server.go @@ -120,17 +120,7 @@ func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvid for _, relationURI := range cfg.Relations { switch relationURI { case relations.OpenIDConnectRel: - rels[relationURI] = relations.OpenIDDiscovery(cfg.IDP) - case relations.OpenIDConnectDesktopRel: - // Handled below - can also be auto-enabled via DesktopIDP config - if cfg.DesktopIDP != "" { - rels[relationURI] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP, cfg.DesktopClientID) - } - case relations.OpenIDConnectMobileRel: - // Handled below - can also be auto-enabled via MobileIDP config - if cfg.MobileIDP != "" { - rels[relationURI] = relations.OpenIDDiscoveryMobile(cfg.MobileIDP, cfg.MobileClientID) - } + rels[relationURI] = relations.OpenIDDiscovery(cfg.IDP, cfg.OIDCClientConfigs) case relations.OpenCloudInstanceRel: var err error rels[relationURI], err = relations.OpenCloudInstance(cfg.Instances, cfg.OpenCloudURL) @@ -142,22 +132,5 @@ func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvid } } - // Auto-enable desktop OIDC issuer when DesktopIDP is configured, - // even if not explicitly listed in Relations. This provides a simpler - // configuration experience - just set WEBFINGER_OIDC_ISSUER_DESKTOP. - // See: https://github.com/opencloud-eu/desktop/issues/246 - if cfg.DesktopIDP != "" { - if _, exists := rels[relations.OpenIDConnectDesktopRel]; !exists { - rels[relations.OpenIDConnectDesktopRel] = relations.OpenIDDiscoveryDesktop(cfg.DesktopIDP, cfg.DesktopClientID) - } - } - - // Auto-enable mobile OIDC issuer when MobileIDP is configured - if cfg.MobileIDP != "" { - if _, exists := rels[relations.OpenIDConnectMobileRel]; !exists { - rels[relations.OpenIDConnectMobileRel] = relations.OpenIDDiscoveryMobile(cfg.MobileIDP, cfg.MobileClientID) - } - } - return rels, nil } diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index bb1d48ac7b..2230a7427d 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -17,15 +17,21 @@ type Config struct { HTTP HTTP `yaml:"http"` - Instances []Instance `yaml:"instances"` - Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A list of relation URIs or registered relation types to add to webfinger responses. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` - IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation." introductionVersion:"1.0.0"` - DesktopIDP string `yaml:"desktop_idp" env:"WEBFINGER_OIDC_ISSUER_DESKTOP" desc:"The identity provider href for desktop clients. When set, desktop clients will use this issuer instead of the default IDP. This allows configuring separate OIDC clients for web and desktop applications." introductionVersion:"%%NEXT%%"` - DesktopClientID string `yaml:"desktop_client_id" env:"WEBFINGER_OIDC_CLIENT_ID_DESKTOP" desc:"The OIDC client ID for desktop clients. When set along with WEBFINGER_OIDC_ISSUER_DESKTOP, this client ID will be provided to desktop clients via webfinger properties." introductionVersion:"%%NEXT%%"` - MobileIDP string `yaml:"mobile_idp" env:"WEBFINGER_OIDC_ISSUER_MOBILE" desc:"The identity provider href for mobile clients. When set, mobile clients will use this issuer instead of the default IDP. This allows configuring separate OIDC clients for web and mobile applications." introductionVersion:"%%NEXT%%"` - MobileClientID string `yaml:"mobile_client_id" env:"WEBFINGER_OIDC_CLIENT_ID_MOBILE" desc:"The OIDC client ID for mobile clients. When set along with WEBFINGER_OIDC_ISSUER_MOBILE, this client ID will be provided to mobile clients via webfinger properties." introductionVersion:"%%NEXT%%"` - OpenCloudURL string `yaml:"opencloud_url" env:"OC_URL;WEBFINGER_OPENCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the legacy OpenCloud server instance relation (not to be confused with the product OpenCloud Server). It defaults to the OC_URL but can be overridden to support some reverse proxy corner cases. To shard the deployment, multiple instances can be configured in the configuration file." introductionVersion:"1.0.0"` - Insecure bool `yaml:"insecure" env:"OC_INSECURE;WEBFINGER_INSECURE" desc:"Allow insecure connections to the WEBFINGER service." introductionVersion:"1.0.0"` + Instances []Instance `yaml:"instances"` + Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A list of relation URIs or registered relation types to add to webfinger responses. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation." introductionVersion:"1.0.0"` + AndroidClientID string `yaml:"android_client_id" env:"WEBFINGER_ANDROID_OIDC_CLIENT_ID" desc:"The OIDC client ID for Android app." introductionVersion:"%%NEXT%%"` + AndroidClientScopes []string `yaml:"android_client_scopes" env:"WEBFINGER_ANDROID_OIDC_CLIENT_SCOPES" desc:"The OIDC client scopes the Android app should request." introductionVersion:"%%NEXT%%"` + DesktopClientID string `yaml:"desktop_client_id" env:"WEBFINGER_DESKTOP_OIDC_CLIENT_ID" desc:"The OIDC client ID for the OpenCloud desktop application." introductionVersion:"%%NEXT%%"` + DesktopClientScopes []string `yaml:"desktop_client_scopes" env:"WEBFINGER_DESKTOP_OIDC_CLIENT_SCOPES" desc:"The OIDC client scopes the OpenCloud desktop application should request." introductionVersion:"%%NEXT%%"` + IOSClientID string `yaml:"ios_client_id" env:"WEBFINGER_IOS_OIDC_CLIENT_ID" desc:"The OIDC client ID for the IOS app." introductionVersion:"%%NEXT%%"` + IOSClientScopes []string `yaml:"ios_client_scopes" env:"WEBFINGER_IOS_OIDC_CLIENT_SCOPES" desc:"The OIDC client scopes the IOS app should request." introductionVersion:"%%NEXT%%"` + WebClientID string `yaml:"web_client_id" env:"WEBFINGER_WEB_OIDC_CLIENT_ID" desc:"The OIDC client ID for the OpenCloud web client." introductionVersion:"%%NEXT%%"` + WebClientScopes []string `yaml:"web_client_scopes" env:"WEBFINGER_WEB_OIDC_CLIENT_SCOPES" desc:"The OIDC client scopes the OpenCloud web client should request." introductionVersion:"%%NEXT%%"` + OpenCloudURL string `yaml:"opencloud_url" env:"OC_URL;WEBFINGER_OPENCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the legacy OpenCloud server instance relation (not to be confused with the product OpenCloud Server). It defaults to the OC_URL but can be overridden to support some reverse proxy corner cases. To shard the deployment, multiple instances can be configured in the configuration file." introductionVersion:"1.0.0"` + Insecure bool `yaml:"insecure" env:"OC_INSECURE;WEBFINGER_INSECURE" desc:"Allow insecure connections to the WEBFINGER service." introductionVersion:"1.0.0"` + + OIDCClientConfigs map[string]OIDCClientConfig `yaml:"-"` Context context.Context `yaml:"-"` } @@ -38,3 +44,8 @@ type Instance struct { Titles map[string]string `yaml:"titles"` Break bool `yaml:"break"` } + +type OIDCClientConfig struct { + ClientID string + Scopes []string +} diff --git a/services/webfinger/pkg/config/defaults/defaultconfig.go b/services/webfinger/pkg/config/defaults/defaultconfig.go index 3d698ce7ff..bc209ae3ca 100644 --- a/services/webfinger/pkg/config/defaults/defaultconfig.go +++ b/services/webfinger/pkg/config/defaults/defaultconfig.go @@ -7,6 +7,11 @@ import ( "github.com/opencloud-eu/opencloud/services/webfinger/pkg/relations" ) +var ( + nativeAppScopes = []string{"openid", "profile", "email", "offline_access"} + webAppScopes = []string{"openid", "profile", "email"} +) + // FullDefaultConfig returns a fully initialized default configuration func FullDefaultConfig() *config.Config { cfg := DefaultConfig() @@ -49,8 +54,16 @@ func DefaultConfig() *config.Config { }, }, }, - IDP: "https://localhost:9200", - Insecure: false, + IDP: "https://localhost:9200", + Insecure: false, + AndroidClientID: "OpenCloudAndroid", + AndroidClientScopes: nativeAppScopes, + DesktopClientID: "OpenCloudDesktop", + DesktopClientScopes: nativeAppScopes, + IOSClientID: "OpenCloudIOS", + IOSClientScopes: nativeAppScopes, + WebClientID: "web", + WebClientScopes: webAppScopes, } } @@ -86,4 +99,23 @@ func Sanitize(cfg *config.Config) { if cfg.HTTP.Root != "/" { cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/") } + + cfg.OIDCClientConfigs = map[string]config.OIDCClientConfig{ + "android": { + ClientID: cfg.AndroidClientID, + Scopes: cfg.AndroidClientScopes, + }, + "desktop": { + ClientID: cfg.DesktopClientID, + Scopes: cfg.DesktopClientScopes, + }, + "ios": { + ClientID: cfg.IOSClientID, + Scopes: cfg.IOSClientScopes, + }, + "web": { + ClientID: cfg.WebClientID, + Scopes: cfg.WebClientScopes, + }, + } } diff --git a/services/webfinger/pkg/relations/owncloud_instance.go b/services/webfinger/pkg/relations/opencloud_instance.go similarity index 95% rename from services/webfinger/pkg/relations/owncloud_instance.go rename to services/webfinger/pkg/relations/opencloud_instance.go index f059748274..1a51ae9c2e 100644 --- a/services/webfinger/pkg/relations/owncloud_instance.go +++ b/services/webfinger/pkg/relations/opencloud_instance.go @@ -57,7 +57,7 @@ func OpenCloudInstance(instances []config.Instance, openCloudURL string) (servic }, nil } -func (l *openCloudInstance) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { +func (l *openCloudInstance) Add(ctx context.Context, _ string, jrd *webfinger.JSONResourceDescriptor) { if jrd == nil { jrd = &webfinger.JSONResourceDescriptor{} } diff --git a/services/webfinger/pkg/relations/owncloud_instance_test.go b/services/webfinger/pkg/relations/opencloud_instance_test.go similarity index 98% rename from services/webfinger/pkg/relations/owncloud_instance_test.go rename to services/webfinger/pkg/relations/opencloud_instance_test.go index fa0aa686f5..8e04c14482 100644 --- a/services/webfinger/pkg/relations/owncloud_instance_test.go +++ b/services/webfinger/pkg/relations/opencloud_instance_test.go @@ -44,7 +44,7 @@ func TestOpenCloudInstanceAddLink(t *testing.T) { "otherclaim": "someone", }) jrd := webfinger.JSONResourceDescriptor{} - provider.Add(ctx, &jrd) + provider.Add(ctx, "", &jrd) if len(jrd.Links) != 1 { t.Errorf("provider returned wrong number of links: %v, expected 1", len(jrd.Links)) diff --git a/services/webfinger/pkg/relations/openid_discovery.go b/services/webfinger/pkg/relations/openid_discovery.go index ea63dae336..a51945e691 100644 --- a/services/webfinger/pkg/relations/openid_discovery.go +++ b/services/webfinger/pkg/relations/openid_discovery.go @@ -3,28 +3,31 @@ package relations import ( "context" + "github.com/opencloud-eu/opencloud/services/webfinger/pkg/config" "github.com/opencloud-eu/opencloud/services/webfinger/pkg/service/v0" "github.com/opencloud-eu/opencloud/services/webfinger/pkg/webfinger" ) const ( - OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" - OpenIDConnectDesktopRel = "http://openid.net/specs/connect/1.0/issuer/desktop" - OpenIDConnectMobileRel = "http://openid.net/specs/connect/1.0/issuer/mobile" + OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" + clientIDProp = "http://opencloud.eu/ns/oidc/client_id" + scopesProp = "http://opencloud.eu/ns/oidc/scopes" ) type openIDDiscovery struct { - Href string + Href string + OIDCClients map[string]config.OIDCClientConfig } // OpenIDDiscovery adds the Openid Connect issuer relation -func OpenIDDiscovery(href string) service.RelationProvider { +func OpenIDDiscovery(href string, clients map[string]config.OIDCClientConfig) service.RelationProvider { return &openIDDiscovery{ - Href: href, + Href: href, + OIDCClients: clients, } } -func (l *openIDDiscovery) Add(_ context.Context, jrd *webfinger.JSONResourceDescriptor) { +func (l *openIDDiscovery) Add(_ context.Context, platform string, jrd *webfinger.JSONResourceDescriptor) { if jrd == nil { jrd = &webfinger.JSONResourceDescriptor{} } @@ -32,73 +35,12 @@ func (l *openIDDiscovery) Add(_ context.Context, jrd *webfinger.JSONResourceDesc Rel: OpenIDConnectRel, Href: l.Href, }) -} - -// ClientIDProperty is the property URI for the OIDC client ID -const ClientIDProperty = "http://openid.net/specs/connect/1.0/client_id" - -type openIDDiscoveryDesktop struct { - Href string - ClientID string -} - -// OpenIDDiscoveryDesktop adds the OpenID Connect issuer relation for desktop clients. -// This allows identity providers that require separate OIDC clients per application type -// (like Authentik, Kanidm, Zitadel) to provide a distinct issuer URL for desktop clients. -// If clientID is provided, it will be included as a property in the link. -// See: https://github.com/opencloud-eu/desktop/issues/246 -func OpenIDDiscoveryDesktop(href string, clientID string) service.RelationProvider { - return &openIDDiscoveryDesktop{ - Href: href, - ClientID: clientID, - } -} - -func (l *openIDDiscoveryDesktop) Add(_ context.Context, jrd *webfinger.JSONResourceDescriptor) { - if jrd == nil { - jrd = &webfinger.JSONResourceDescriptor{} - } - link := webfinger.Link{ - Rel: OpenIDConnectDesktopRel, - Href: l.Href, - } - if l.ClientID != "" { - link.Properties = map[string]string{ - ClientIDProperty: l.ClientID, - } - } - jrd.Links = append(jrd.Links, link) -} -type openIDDiscoveryMobile struct { - Href string - ClientID string -} - -// OpenIDDiscoveryMobile adds the OpenID Connect issuer relation for mobile clients. -// This allows identity providers that require separate OIDC clients per application type -// (like Authentik, Kanidm, Zitadel) to provide a distinct issuer URL for mobile clients. -// If clientID is provided, it will be included as a property in the link. -// See: https://github.com/opencloud-eu/desktop/issues/246 -func OpenIDDiscoveryMobile(href string, clientID string) service.RelationProvider { - return &openIDDiscoveryMobile{ - Href: href, - ClientID: clientID, - } -} - -func (l *openIDDiscoveryMobile) Add(_ context.Context, jrd *webfinger.JSONResourceDescriptor) { - if jrd == nil { - jrd = &webfinger.JSONResourceDescriptor{} - } - link := webfinger.Link{ - Rel: OpenIDConnectMobileRel, - Href: l.Href, - } - if l.ClientID != "" { - link.Properties = map[string]string{ - ClientIDProperty: l.ClientID, + if platform != "" { + if clientConfig, ok := l.OIDCClients[platform]; ok { + jrd.Properties = make(map[string]any) + jrd.Properties[clientIDProp] = clientConfig.ClientID + jrd.Properties[scopesProp] = clientConfig.Scopes } } - jrd.Links = append(jrd.Links, link) } diff --git a/services/webfinger/pkg/relations/openid_discovery_test.go b/services/webfinger/pkg/relations/openid_discovery_test.go index 7b4efabd44..097076763b 100644 --- a/services/webfinger/pkg/relations/openid_discovery_test.go +++ b/services/webfinger/pkg/relations/openid_discovery_test.go @@ -4,15 +4,27 @@ import ( "context" "testing" + "github.com/opencloud-eu/opencloud/services/webfinger/pkg/config" "github.com/opencloud-eu/opencloud/services/webfinger/pkg/webfinger" ) func TestOpenidDiscovery(t *testing.T) { - provider := OpenIDDiscovery("http://issuer.url") + clients := map[string]config.OIDCClientConfig{ + "web": { + ClientID: "web", + Scopes: []string{"openid", "profile", "email"}, + }, + "test": { + ClientID: "test", + Scopes: []string{"test"}, + }, + } + + provider := OpenIDDiscovery("http://issuer.url", clients) jrd := webfinger.JSONResourceDescriptor{} - provider.Add(context.Background(), &jrd) + provider.Add(context.Background(), "", &jrd) if len(jrd.Links) != 1 { t.Errorf("provider returned wrong number of links: %v, expected 1", len(jrd.Links)) @@ -23,4 +35,19 @@ func TestOpenidDiscovery(t *testing.T) { if jrd.Links[0].Rel != "http://openid.net/specs/connect/1.0/issuer" { t.Errorf("provider returned wrong openid connect rel: %v, expected %v", jrd.Links[0].Href, OpenIDConnectRel) } + if len(jrd.Properties) != 0 { + t.Errorf("provider returned properties for empty platform: %v, expected 0", len(jrd.Properties)) + } + + jrd = webfinger.JSONResourceDescriptor{} + provider.Add(context.Background(), "test", &jrd) + if len(jrd.Properties) != 2 { + t.Errorf("provider returned wrong number of properties for platform test: %v, expected 2", len(jrd.Properties)) + } + if jrd.Properties["http://opencloud.eu/ns/oidc/client_id"] != "test" { + t.Errorf("provider returned wrong client_id property: %v, expected %v", jrd.Properties["http://opencloud.eu/ns/oidc/client_id"], "test") + } + if scopes, ok := jrd.Properties["http://opencloud.eu/ns/oidc/scopes"].([]string); !ok || len(scopes) != 1 || scopes[0] != "test" { + t.Errorf("provider returned wrong scopes property: %v, expected %v", jrd.Properties["http://opencloud.eu/ns/oidc/scopes"], []string{"test"}) + } } diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index 0f01583c88..dbe91b0ca4 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -127,7 +127,9 @@ func WebfingerHandler(service svc.Service) func(w http.ResponseWriter, r *http.R rels := r.URL.Query()["rel"] - jrd, err := service.Webfinger(ctx, queryTarget, rels) + platform := r.URL.Query().Get("platform") + + jrd, err := service.Webfinger(ctx, queryTarget, rels, platform) if errors.Is(err, serviceErrors.ErrNotFound) { // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 // diff --git a/services/webfinger/pkg/service/v0/instrument.go b/services/webfinger/pkg/service/v0/instrument.go index bfca78a3df..b5a34c9dbf 100644 --- a/services/webfinger/pkg/service/v0/instrument.go +++ b/services/webfinger/pkg/service/v0/instrument.go @@ -23,7 +23,7 @@ type instrument struct { } // Webfinger implements the Service interface. -func (i instrument) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) { +func (i instrument) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string, platform string) (webfinger.JSONResourceDescriptor, error) { timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { us := v * 1000000 @@ -35,5 +35,5 @@ func (i instrument) Webfinger(ctx context.Context, queryTarget *url.URL, rels [] i.metrics.Counter.WithLabelValues().Inc() - return i.next.Webfinger(ctx, queryTarget, rels) + return i.next.Webfinger(ctx, queryTarget, rels, platform) } diff --git a/services/webfinger/pkg/service/v0/logging.go b/services/webfinger/pkg/service/v0/logging.go index 4f03b76958..270bc3abd3 100644 --- a/services/webfinger/pkg/service/v0/logging.go +++ b/services/webfinger/pkg/service/v0/logging.go @@ -22,11 +22,11 @@ type logging struct { } // Webfinger implements the Service interface. -func (l logging) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) { +func (l logging) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string, platform string) (webfinger.JSONResourceDescriptor, error) { l.logger.Debug(). Str("query_target", queryTarget.String()). Strs("rel", rels). Msg("Webfinger") - return l.next.Webfinger(ctx, queryTarget, rels) + return l.next.Webfinger(ctx, queryTarget, rels, platform) } diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 486eb1cb82..29c694d84d 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -45,11 +45,11 @@ type Service interface { // } // ] // } - Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) + Webfinger(ctx context.Context, queryTarget *url.URL, rels []string, platform string) (webfinger.JSONResourceDescriptor, error) } type RelationProvider interface { - Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) + Add(ctx context.Context, platform string, jrd *webfinger.JSONResourceDescriptor) } // New returns a new instance of Service @@ -81,7 +81,7 @@ type svc struct { // - one that looks up in instance by id (use template, read from json, read from ldap, read from graph) // Webfinger implements the service interface -func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string) (webfinger.JSONResourceDescriptor, error) { +func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string, platform string) (webfinger.JSONResourceDescriptor, error) { jrd := webfinger.JSONResourceDescriptor{ Subject: queryTarget.String(), @@ -90,13 +90,13 @@ func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string) if len(rel) == 0 { // add all configured relation providers for _, relation := range s.relationProviders { - relation.Add(ctx, &jrd) + relation.Add(ctx, platform, &jrd) } } else { // only add requested relations for _, r := range rel { if relation, ok := s.relationProviders[r]; ok { - relation.Add(ctx, &jrd) + relation.Add(ctx, platform, &jrd) } } } diff --git a/services/webfinger/pkg/service/v0/tracing.go b/services/webfinger/pkg/service/v0/tracing.go index fd41cce948..4c3557bc4d 100644 --- a/services/webfinger/pkg/service/v0/tracing.go +++ b/services/webfinger/pkg/service/v0/tracing.go @@ -23,7 +23,7 @@ type tracing struct { } // Webfinger implements the Service interface. -func (t tracing) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) { +func (t tracing) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string, platform string) (webfinger.JSONResourceDescriptor, error) { spanOpts := []trace.SpanStartOption{ trace.WithSpanKind(trace.SpanKindServer), trace.WithAttributes( @@ -34,5 +34,5 @@ func (t tracing) Webfinger(ctx context.Context, queryTarget *url.URL, rels []str ctx, span := t.tp.Tracer("webfinger").Start(ctx, "Webfinger", spanOpts...) defer span.End() - return t.next.Webfinger(ctx, queryTarget, rels) + return t.next.Webfinger(ctx, queryTarget, rels, platform) } diff --git a/services/webfinger/pkg/webfinger/webfinger.go b/services/webfinger/pkg/webfinger/webfinger.go index f4674376ea..531a2523c2 100644 --- a/services/webfinger/pkg/webfinger/webfinger.go +++ b/services/webfinger/pkg/webfinger/webfinger.go @@ -56,7 +56,7 @@ type JSONResourceDescriptor struct { // values are strings or null. // // The "properties" member is OPTIONAL in the JRD. - Properties map[string]string `json:"properties,omitempty"` + Properties map[string]any `json:"properties,omitempty"` // Links is an array of objects that contain link relation information // // The "links" array is OPTIONAL in the JRD.