From 6579df299a037b6159a98c0b0195de3518dbc56a Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sun, 18 Jan 2026 06:03:25 +0000 Subject: [PATCH] Find identity providers by brand name when unique. Signed-off-by: Jason Volk --- src/core/config/mod.rs | 14 ++++++-- src/service/oauth/providers.rs | 65 +++++++++++++++++++++++++++++----- tuwunel-example.toml | 14 ++++++-- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index cb501c82..ad90a39e 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2525,8 +2525,13 @@ pub struct IdentityProvider { /// for your convenience. For certain brands we apply essential internal /// workarounds specific to that provider; it is important to configure this /// field properly when a provider needs to be recognized (like GitHub for - /// example). Several configured providers can share the same brand name. It - /// is not case-sensitive. + /// example). + /// + /// Several configured providers can share the same brand name. It is not + /// case-sensitive. As a convenience for common simple deployments we can + /// identify this provider by brand in addition to the unique `client_id` if + /// and only if there is a single provider for the brand; see notes for + /// `client_id`. #[serde(deserialize_with = "utils::string::de::to_lowercase")] pub brand: String, @@ -2534,6 +2539,11 @@ pub struct IdentityProvider { /// registration. This ID then uniquely identifies this configuration /// instance itself, becoming the identity provider's ID and must be unique /// and remain unchanged. + /// + /// As a convenience we also identify this config by `brand` if and only if + /// there is a single provider configured for a `brand`. Note carefully that + /// multiple providers configured with the same `brand` is not an error and + /// this provider will simply not be found when querying by `brand`. pub client_id: String, /// Secret key the provider generated for you along with the `client_id` diff --git a/src/service/oauth/providers.rs b/src/service/oauth/providers.rs index e419510e..4f295f7d 100644 --- a/src/service/oauth/providers.rs +++ b/src/service/oauth/providers.rs @@ -28,33 +28,80 @@ pub(super) fn build(args: &crate::Args<'_>) -> Self { #[implement(Providers)] #[tracing::instrument(level = "debug", skip(self))] pub async fn get(&self, id: &str) -> Result { - if let Some(provider) = self.providers.read().await.get(id).cloned() { + if let Some(provider) = self.get_cached(id).await { return Ok(provider); } let config = self.get_config(id)?; + let id = config.id().to_owned(); let mut map = self.providers.write().await; - let config = self.configure(config).await?; + let provider = self.configure(config).await?; - debug!(?id, ?config); - _ = map.insert(id.into(), config.clone()); + debug!(?id, ?provider); + _ = map.insert(id, provider.clone()); - Ok(config) + Ok(provider) } /// Get the admin-configured Provider which exists prior to any /// reconciliation with the well-known discovery (the server's config is /// immutable); though it is important to note the server config can be /// reloaded. This will Err NotFound for a non-existent idp. +/// +/// When no provider is found with a matching client_id, providers are then +/// searched by brand. Brand matching will be invalidated when more than one +/// provider matches the brand. #[implement(Providers)] pub fn get_config(&self, id: &str) -> Result { - self.services - .config - .identity_provider + let providers = &self.services.config.identity_provider; + + if let Some(provider) = providers .iter() .find(|config| config.id() == id) .cloned() - .ok_or_else(|| err!(Request(NotFound("Unrecognized Identity Provider")))) + { + return Ok(provider); + } + + if let Some(provider) = providers + .iter() + .find(|config| config.brand == id.to_lowercase()) + .filter(|_| { + providers + .iter() + .filter(|config| config.brand == id.to_lowercase()) + .count() + .eq(&1) + }) + .cloned() + { + return Ok(provider); + } + + Err!(Request(NotFound("Unrecognized Identity Provider"))) +} + +/// Get the discovered provider from the runtime cache. ID may be client_id or +/// brand if brand is unique among provider configurations. +#[implement(Providers)] +async fn get_cached(&self, id: &str) -> Option { + let providers = self.providers.read().await; + + if let Some(provider) = providers.get(id).cloned() { + return Some(provider); + } + + providers + .values() + .find(|provider| provider.brand == id.to_lowercase()) + .filter(|_| { + providers + .values() + .filter(|provider| provider.brand == id.to_lowercase()) + .count() + .eq(&1) + }) + .cloned() } /// Configure an identity provider; takes the admin-configured instance from the diff --git a/tuwunel-example.toml b/tuwunel-example.toml index 84572bc6..00ead166 100644 --- a/tuwunel-example.toml +++ b/tuwunel-example.toml @@ -2141,8 +2141,13 @@ # for your convenience. For certain brands we apply essential internal # workarounds specific to that provider; it is important to configure this # field properly when a provider needs to be recognized (like GitHub for -# example). Several configured providers can share the same brand name. It -# is not case-sensitive. +# example). +# +# Several configured providers can share the same brand name. It is not +# case-sensitive. As a convenience for common simple deployments we can +# identify this provider by brand in addition to the unique `client_id` if +# and only if there is a single provider for the brand; see notes for +# `client_id`. # #brand = @@ -2151,6 +2156,11 @@ # instance itself, becoming the identity provider's ID and must be unique # and remain unchanged. # +# As a convenience we also identify this config by `brand` if and only if +# there is a single provider configured for a `brand`. Note carefully that +# multiple providers configured with the same `brand` is not an error and +# this provider will simply not be found when querying by `brand`. +# #client_id = # Secret key the provider generated for you along with the `client_id`