From 989827e3b552769300f885cbec9392cfe217a65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20Teijelo=20P=C3=A9rez?= Date: Mon, 20 Apr 2026 23:48:13 -0400 Subject: [PATCH] feat: add OTHER_AUDIENCES config (#2605) Co-authored-by: GatewayJ <835269233@qq.com> --- crates/config/src/constants/oidc.rs | 6 +++- crates/ecstore/src/config/com.rs | 30 ++++++++++++----- crates/ecstore/src/config/oidc.rs | 9 +++-- crates/iam/src/oidc.rs | 51 +++++++++++++++++++++++++---- rustfs/src/admin/handlers/oidc.rs | 12 ++++++- 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/crates/config/src/constants/oidc.rs b/crates/config/src/constants/oidc.rs index 493024737..b7d3c12c7 100644 --- a/crates/config/src/constants/oidc.rs +++ b/crates/config/src/constants/oidc.rs @@ -17,6 +17,7 @@ pub const OIDC_CONFIG_URL: &str = "config_url"; pub const OIDC_CLIENT_ID: &str = "client_id"; pub const OIDC_CLIENT_SECRET: &str = "client_secret"; pub const OIDC_SCOPES: &str = "scopes"; +pub const OIDC_OTHER_AUDIENCES: &str = "other_audiences"; pub const OIDC_REDIRECT_URI: &str = "redirect_uri"; pub const OIDC_REDIRECT_URI_DYNAMIC: &str = "redirect_uri_dynamic"; pub const OIDC_CLAIM_NAME: &str = "claim_name"; @@ -34,6 +35,7 @@ pub const ENV_IDENTITY_OPENID_CONFIG_URL: &str = "RUSTFS_IDENTITY_OPENID_CONFIG_ pub const ENV_IDENTITY_OPENID_CLIENT_ID: &str = "RUSTFS_IDENTITY_OPENID_CLIENT_ID"; pub const ENV_IDENTITY_OPENID_CLIENT_SECRET: &str = "RUSTFS_IDENTITY_OPENID_CLIENT_SECRET"; pub const ENV_IDENTITY_OPENID_SCOPES: &str = "RUSTFS_IDENTITY_OPENID_SCOPES"; +pub const ENV_IDENTITY_OPENID_OTHER_AUDIENCES: &str = "RUSTFS_IDENTITY_OPENID_OTHER_AUDIENCES"; pub const ENV_IDENTITY_OPENID_REDIRECT_URI: &str = "RUSTFS_IDENTITY_OPENID_REDIRECT_URI"; pub const ENV_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC: &str = "RUSTFS_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC"; pub const ENV_IDENTITY_OPENID_CLAIM_NAME: &str = "RUSTFS_IDENTITY_OPENID_CLAIM_NAME"; @@ -46,12 +48,13 @@ pub const ENV_IDENTITY_OPENID_EMAIL_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_EMAIL_ pub const ENV_IDENTITY_OPENID_USERNAME_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_USERNAME_CLAIM"; /// List of all environment variable keys for an OIDC provider. -pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 15] = &[ +pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 16] = &[ ENV_IDENTITY_OPENID_ENABLE, ENV_IDENTITY_OPENID_CONFIG_URL, ENV_IDENTITY_OPENID_CLIENT_ID, ENV_IDENTITY_OPENID_CLIENT_SECRET, ENV_IDENTITY_OPENID_SCOPES, + ENV_IDENTITY_OPENID_OTHER_AUDIENCES, ENV_IDENTITY_OPENID_REDIRECT_URI, ENV_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC, ENV_IDENTITY_OPENID_CLAIM_NAME, @@ -71,6 +74,7 @@ pub const IDENTITY_OPENID_KEYS: &[&str] = &[ OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_SCOPES, + OIDC_OTHER_AUDIENCES, OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_CLAIM_NAME, diff --git a/crates/ecstore/src/config/com.rs b/crates/ecstore/src/config/com.rs index 05ba080f9..cc3c0de25 100644 --- a/crates/ecstore/src/config/com.rs +++ b/crates/ecstore/src/config/com.rs @@ -40,6 +40,8 @@ const CONFIG_FILE: &str = "config.json"; pub const STORAGE_CLASS_SUB_SYS: &str = "storage_class"; +pub const COMMA_SEPARATED_LISTS: &[&str] = &[rustfs_config::oidc::OIDC_SCOPES, rustfs_config::oidc::OIDC_OTHER_AUDIENCES]; + static CONFIG_BUCKET: LazyLock = LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR}{CONFIG_PREFIX}")); static SUB_SYSTEMS_DYNAMIC: LazyLock> = LazyLock::new(|| { @@ -264,15 +266,15 @@ fn parse_oidc_scalar_value(key: &str, value: &Value) -> Option { }), Value::Bool(v) => Some(v.to_string()), Value::Number(v) => Some(v.to_string()), - Value::Array(values) if key == rustfs_config::oidc::OIDC_SCOPES => { - let scopes = values + Value::Array(values) if COMMA_SEPARATED_LISTS.contains(&key) => { + let values_str = values .iter() .filter_map(Value::as_str) .map(str::trim) - .filter(|scope| !scope.is_empty()) + .filter(|val| !val.is_empty()) .collect::>() .join(","); - Some(scopes) + Some(values_str) } Value::Null => None, _ => None, @@ -570,15 +572,15 @@ fn build_oidc_provider_object(kvs: &KVS) -> Map { continue; } - if kv.key == rustfs_config::oidc::OIDC_SCOPES { - let scopes = kv + if COMMA_SEPARATED_LISTS.contains(&kv.key.as_str()) { + let values = kv .value .split(',') .map(str::trim) - .filter(|scope| !scope.is_empty()) - .map(|scope| Value::String(scope.to_string())) + .filter(|val| !val.is_empty()) + .map(|val| Value::String(val.to_string())) .collect::>(); - provider.insert(kv.key.clone(), Value::Array(scopes)); + provider.insert(kv.key.clone(), Value::Array(values)); continue; } @@ -1689,6 +1691,7 @@ mod tests { "client_id":"console", "client_secret":"secret-value", "scopes":["openid","profile","email"], + "other_audiences":["aud1", "aud2"], "redirect_uri_dynamic":true, "display_name":"Default Provider" }, @@ -1713,6 +1716,7 @@ mod tests { ); assert_eq!(default_kvs.get(rustfs_config::oidc::OIDC_CLIENT_ID), "console"); assert_eq!(default_kvs.get(rustfs_config::oidc::OIDC_SCOPES), "openid,profile,email"); + assert_eq!(default_kvs.get(rustfs_config::oidc::OIDC_OTHER_AUDIENCES), "aud1,aud2"); assert_eq!(default_kvs.get(ENABLE_KEY), EnableState::On.to_string()); let smoke_kvs = cfg @@ -1897,6 +1901,7 @@ mod tests { ); default_provider.insert(rustfs_config::oidc::OIDC_CLIENT_ID.to_string(), "console".to_string()); default_provider.insert(rustfs_config::oidc::OIDC_SCOPES.to_string(), "openid,profile,email".to_string()); + default_provider.insert(rustfs_config::oidc::OIDC_OTHER_AUDIENCES.to_string(), "aud1,aud2".to_string()); oidc_section.insert(DEFAULT_DELIMITER.to_string(), default_provider); cfg.0.insert(IDENTITY_OPENID_SUB_SYS.to_string(), oidc_section); @@ -1924,6 +1929,13 @@ mod tests { .map(|values| values.iter().filter_map(Value::as_str).collect::>()), Some(vec!["openid", "profile", "email"]) ); + assert_eq!( + default_provider + .get(rustfs_config::oidc::OIDC_OTHER_AUDIENCES) + .and_then(Value::as_array) + .map(|values| values.iter().filter_map(Value::as_str).collect::>()), + Some(vec!["aud1", "aud2"]) + ); assert_eq!(default_provider.get(ENABLE_KEY).and_then(Value::as_bool), Some(true)); } diff --git a/crates/ecstore/src/config/oidc.rs b/crates/ecstore/src/config/oidc.rs index c89ffb8f3..65325dbea 100644 --- a/crates/ecstore/src/config/oidc.rs +++ b/crates/ecstore/src/config/oidc.rs @@ -18,8 +18,8 @@ use rustfs_config::{ oidc::{ OIDC_CLAIM_NAME, OIDC_CLAIM_PREFIX, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CONFIG_URL, OIDC_DEFAULT_CLAIM_NAME, OIDC_DEFAULT_EMAIL_CLAIM, OIDC_DEFAULT_GROUPS_CLAIM, OIDC_DEFAULT_ROLES_CLAIM, OIDC_DEFAULT_SCOPES, - OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_REDIRECT_URI, - OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES, OIDC_USERNAME_CLAIM, + OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_OTHER_AUDIENCES, + OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES, OIDC_USERNAME_CLAIM, }, }; use std::sync::LazyLock; @@ -52,6 +52,11 @@ pub static DEFAULT_IDENTITY_OPENID_KVS: LazyLock = LazyLock::new(|| { value: OIDC_DEFAULT_SCOPES.to_owned(), hidden_if_empty: false, }, + KV { + key: OIDC_OTHER_AUDIENCES.to_owned(), + value: "".to_owned(), + hidden_if_empty: false, + }, KV { key: OIDC_REDIRECT_URI.to_owned(), value: "".to_owned(), diff --git a/crates/iam/src/oidc.rs b/crates/iam/src/oidc.rs index ace5034e6..afc46c2f8 100644 --- a/crates/iam/src/oidc.rs +++ b/crates/iam/src/oidc.rs @@ -21,8 +21,8 @@ use crate::oidc_state::{OidcAuthSession, OidcStateStore}; use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreIdToken, CoreProviderMetadata}; use openidconnect::{ - AsyncHttpClient, AuthType, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, - PkceCodeVerifier, RedirectUrl, Scope, + AsyncHttpClient, Audience, AuthType, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, + PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, }; use reqwest::Client; use rustfs_config::oidc::*; @@ -157,6 +157,7 @@ pub struct OidcProviderConfig { pub client_id: String, pub client_secret: Option, pub scopes: Vec, + pub other_audiences: Vec, pub redirect_uri: Option, pub redirect_uri_dynamic: bool, pub claim_name: String, @@ -235,6 +236,15 @@ pub struct OidcSys { http_client: ReqwestHttpClient, } +fn trusted_aud(other_audiences: &[String], audience: &Audience) -> bool { + for aud in other_audiences { + if audience.as_str() == aud.as_str() { + return true; + } + } + false +} + impl OidcSys { /// Parse environment variables and discover all configured OIDC providers. pub async fn new() -> Result { @@ -394,7 +404,9 @@ impl OidcSys { .id_token() .ok_or_else(|| "no id_token in token response".to_string())?; - let verifier = client.id_token_verifier(); + let verifier = client + .id_token_verifier() + .set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud)); let verified = id_token.claims(&verifier, &Nonce::new(session.nonce.clone())); if let Err(e) = verified { let refreshed_state = self @@ -416,7 +428,9 @@ impl OidcSys { ) .set_auth_type(AuthType::RequestBody); - let verifier = client.id_token_verifier(); + let verifier = client + .id_token_verifier() + .set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud)); id_token .claims(&verifier, &Nonce::new(session.nonce.clone())) .map_err(|retry_err| format!("ID token verification failed after JWKS refresh: {retry_err}"))?; @@ -529,7 +543,9 @@ impl OidcSys { // Verify the token (signature, issuer, audience, expiry) — skip nonce // (nonce is only required for the authorization code flow) - let verifier = client.id_token_verifier(); + let verifier = client + .id_token_verifier() + .set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud)); if let Err(e) = id_token.claims(&verifier, |_: Option<&Nonce>| Ok(())) { state = self .refresh_provider_state(&provider_id, &config) @@ -544,7 +560,9 @@ impl OidcSys { config.client_secret.as_ref().map(|s| ClientSecret::new(s.clone())), ) .set_auth_type(AuthType::RequestBody); - let verifier = client.id_token_verifier(); + let verifier = client + .id_token_verifier() + .set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud)); id_token .claims(&verifier, |_: Option<&Nonce>| Ok(())) .map_err(|retry_err| format!("ID token verification failed after JWKS refresh: {retry_err}"))?; @@ -742,6 +760,14 @@ impl OidcSys { scopes_str.split(',').map(|s| s.trim().to_string()).collect() }; + let other_audiences_str = get_env(ENV_IDENTITY_OPENID_OTHER_AUDIENCES); + let other_audiences = other_audiences_str + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + let redirect_uri_dynamic_str = get_env(ENV_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC); let redirect_uri_dynamic = redirect_uri_dynamic_str.is_empty() || redirect_uri_dynamic_str @@ -802,6 +828,7 @@ impl OidcSys { client_id: get_env(ENV_IDENTITY_OPENID_CLIENT_ID), client_secret, scopes, + other_audiences, redirect_uri, redirect_uri_dynamic, claim_name, @@ -835,6 +862,14 @@ impl OidcSys { scopes_str.split(',').map(|s| s.trim().to_string()).collect() }; + let other_audiences_str = kvs.get(OIDC_OTHER_AUDIENCES); + let other_audiences = other_audiences_str + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + let redirect_uri_dynamic = kvs .lookup(OIDC_REDIRECT_URI_DYNAMIC) .unwrap_or_else(|| EnableState::On.to_string()) @@ -868,6 +903,7 @@ impl OidcSys { client_id: kvs.get(OIDC_CLIENT_ID), client_secret, scopes, + other_audiences, redirect_uri, redirect_uri_dynamic, claim_name, @@ -1352,6 +1388,7 @@ mod tests { client_id: "rustfs-oidc-test".to_string(), client_secret: None, scopes: vec!["openid".to_string()], + other_audiences: vec![], redirect_uri: None, redirect_uri_dynamic: false, claim_name: "sub".to_string(), @@ -1717,6 +1754,7 @@ mod tests { client_id: "client-id".to_string(), client_secret: None, scopes: vec!["openid".to_string()], + other_audiences: vec![], redirect_uri: None, redirect_uri_dynamic: true, claim_name: "groups".to_string(), @@ -1811,6 +1849,7 @@ mod tests { client_id: "my-client".to_string(), client_secret: Some("secret".to_string()), scopes: vec!["openid".to_string(), "profile".to_string(), "email".to_string()], + other_audiences: vec![], redirect_uri: None, redirect_uri_dynamic: true, claim_name: "groups".to_string(), diff --git a/rustfs/src/admin/handlers/oidc.rs b/rustfs/src/admin/handlers/oidc.rs index 6907d0161..bd8df94b4 100644 --- a/rustfs/src/admin/handlers/oidc.rs +++ b/rustfs/src/admin/handlers/oidc.rs @@ -23,7 +23,7 @@ use matchit::Params; use rustfs_config::oidc::{ IDENTITY_OPENID_SUB_SYS, OIDC_CLAIM_NAME, OIDC_CLAIM_PREFIX, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CONFIG_URL, OIDC_DEFAULT_CLAIM_NAME, OIDC_DEFAULT_EMAIL_CLAIM, OIDC_DEFAULT_GROUPS_CLAIM, OIDC_DEFAULT_ROLES_CLAIM, OIDC_DEFAULT_SCOPES, - OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_REDIRECT_URI, + OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_OTHER_AUDIENCES, OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES, OIDC_USERNAME_CLAIM, }; use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE}; @@ -126,6 +126,7 @@ struct OidcConfigView { client_id: String, client_secret_configured: bool, scopes: Vec, + other_audiences: Vec, redirect_uri: Option, redirect_uri_dynamic: bool, claim_name: String, @@ -162,6 +163,7 @@ struct OidcConfigUpsertRequest { client_id: String, client_secret: Option, scopes: Vec, + other_audiences: Vec, redirect_uri: Option, redirect_uri_dynamic: bool, claim_name: String, @@ -182,6 +184,7 @@ impl Default for OidcConfigUpsertRequest { client_id: String::new(), client_secret: None, scopes: OIDC_DEFAULT_SCOPES.split(',').map(ToString::to_string).collect(), + other_audiences: Vec::new(), redirect_uri: None, redirect_uri_dynamic: true, claim_name: OIDC_DEFAULT_CLAIM_NAME.to_string(), @@ -205,6 +208,7 @@ struct OidcConfigValidateRequest { client_id: String, client_secret: Option, scopes: Vec, + other_audiences: Vec, redirect_uri: Option, redirect_uri_dynamic: bool, claim_name: String, @@ -226,6 +230,7 @@ impl Default for OidcConfigValidateRequest { client_id: String::new(), client_secret: None, scopes: OIDC_DEFAULT_SCOPES.split(',').map(ToString::to_string).collect(), + other_audiences: Vec::new(), redirect_uri: None, redirect_uri_dynamic: true, claim_name: OIDC_DEFAULT_CLAIM_NAME.to_string(), @@ -280,6 +285,7 @@ impl Operation for GetOidcConfigHandler { client_id: provider.config.client_id.clone(), client_secret_configured: provider.config.client_secret.is_some(), scopes: provider.config.scopes.clone(), + other_audiences: provider.config.other_audiences.clone(), redirect_uri: provider.config.redirect_uri.clone(), redirect_uri_dynamic: provider.config.redirect_uri_dynamic, claim_name: provider.config.claim_name.clone(), @@ -785,6 +791,7 @@ fn build_provider_config_from_upsert( client_id: request.client_id.trim().to_string(), client_secret, scopes, + other_audiences: request.other_audiences, redirect_uri: normalize_optional(request.redirect_uri), redirect_uri_dynamic: request.redirect_uri_dynamic, claim_name: if request.claim_name.trim().is_empty() { @@ -836,6 +843,7 @@ fn build_provider_config_from_validate( client_id: request.client_id.trim().to_string(), client_secret: request.client_secret.filter(|value| !value.trim().is_empty()), scopes: normalize_scopes(&request.scopes), + other_audiences: request.other_audiences, redirect_uri: normalize_optional(request.redirect_uri), redirect_uri_dynamic: request.redirect_uri_dynamic, claim_name: if request.claim_name.trim().is_empty() { @@ -902,6 +910,7 @@ fn upsert_persisted_provider_config(config: &mut ServerConfig, provider_config: set_kvs_value(&mut kvs, OIDC_CLIENT_ID, provider_config.client_id.clone()); set_kvs_value(&mut kvs, OIDC_CLIENT_SECRET, provider_config.client_secret.clone().unwrap_or_default()); set_kvs_value(&mut kvs, OIDC_SCOPES, provider_config.scopes.join(",")); + set_kvs_value(&mut kvs, OIDC_OTHER_AUDIENCES, provider_config.other_audiences.join(",")); set_kvs_value(&mut kvs, OIDC_REDIRECT_URI, provider_config.redirect_uri.clone().unwrap_or_default()); set_kvs_value( &mut kvs, @@ -1170,6 +1179,7 @@ mod tests { client_id: "console".to_string(), client_secret: Some("secret".to_string()), scopes: vec!["openid".to_string(), "profile".to_string()], + other_audiences: vec![], redirect_uri: None, redirect_uri_dynamic: true, claim_name: OIDC_DEFAULT_CLAIM_NAME.to_string(),