feat: add OTHER_AUDIENCES config (#2605)

Co-authored-by: GatewayJ <835269233@qq.com>
This commit is contained in:
Andy Teijelo Pérez
2026-04-20 23:48:13 -04:00
committed by GitHub
parent a77be8f89b
commit 989827e3b5
5 changed files with 89 additions and 19 deletions
+5 -1
View File
@@ -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,
+21 -9
View File
@@ -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<String> = LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR}{CONFIG_PREFIX}"));
static SUB_SYSTEMS_DYNAMIC: LazyLock<HashSet<String>> = LazyLock::new(|| {
@@ -264,15 +266,15 @@ fn parse_oidc_scalar_value(key: &str, value: &Value) -> Option<String> {
}),
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::<Vec<_>>()
.join(",");
Some(scopes)
Some(values_str)
}
Value::Null => None,
_ => None,
@@ -570,15 +572,15 @@ fn build_oidc_provider_object(kvs: &KVS) -> Map<String, Value> {
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::<Vec<_>>();
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::<Vec<_>>()),
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::<Vec<_>>()),
Some(vec!["aud1", "aud2"])
);
assert_eq!(default_provider.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
}
+7 -2
View File
@@ -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<KVS> = 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(),
+45 -6
View File
@@ -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<String>,
pub scopes: Vec<String>,
pub other_audiences: Vec<String>,
pub redirect_uri: Option<String>,
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<Self, String> {
@@ -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(),
+11 -1
View File
@@ -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<String>,
other_audiences: Vec<String>,
redirect_uri: Option<String>,
redirect_uri_dynamic: bool,
claim_name: String,
@@ -162,6 +163,7 @@ struct OidcConfigUpsertRequest {
client_id: String,
client_secret: Option<String>,
scopes: Vec<String>,
other_audiences: Vec<String>,
redirect_uri: Option<String>,
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<String>,
scopes: Vec<String>,
other_audiences: Vec<String>,
redirect_uri: Option<String>,
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(),