use crate::{
cache::Cache,
errors::{Error, Result},
tenant::{TenantConfig, TenantResolution},
http_tenant::HttpClientWithAdminSupport,
types::OidcProviderMetadata,
};
const DEFAULT_DISCOVERY_CACHE_TTL: u64 = 600;
#[tracing::instrument(
name = "oidc_discover_tenant_resolution",
skip(http, cache),
fields(
issuer = %issuer,
resolved_tenant = ?tenant_resolution.resolve()
)
)]
pub async fn discover_with_tenant_resolution(
issuer: &str,
tenant_resolution: &TenantResolution,
http: &dyn HttpClientWithAdminSupport,
cache: &dyn Cache<String, OidcProviderMetadata>,
) -> Result<OidcProviderMetadata> {
tracing::info!(
target: "xjp_oidc::discovery",
"开始多租户 OIDC 发现, resolved_tenant: {:?}",
tenant_resolution.resolve()
);
if issuer.is_empty() {
return Err(Error::InvalidParam("issuer cannot be empty"));
}
let cache_key = format!(
"discovery:{}:{}",
issuer,
tenant_resolution.tenant_id()
);
if let Some(cached) = cache.get(&cache_key) {
tracing::debug!("Using cached discovery metadata");
return Ok(cached);
}
let discovery_url = build_discovery_url(issuer)?;
let final_url = if let Some(client_id) = &tenant_resolution.client_id_tenant {
let separator = if discovery_url.contains('?') { "&" } else { "?" };
format!("{}{}client_id={}", discovery_url, separator, client_id)
} else {
discovery_url
};
let value = http
.get_value_with_admin_override(&final_url, tenant_resolution.admin_override_tenant.as_deref())
.await?;
if let Some(error) = value.get("error").and_then(|v| v.as_str()) {
let error_description = value
.get("error_description")
.and_then(|v| v.as_str())
.unwrap_or("No description");
return Err(Error::Discovery(format!(
"Discovery failed: {} - {}",
error, error_description
)));
}
let metadata: OidcProviderMetadata = serde_json::from_value(value)
.map_err(|e| Error::Discovery(format!("Failed to parse discovery metadata: {}", e)))?;
validate_metadata(&metadata, issuer)?;
cache.put(cache_key, metadata.clone(), DEFAULT_DISCOVERY_CACHE_TTL);
tracing::info!("Discovery completed successfully");
Ok(metadata)
}
fn build_discovery_url(issuer: &str) -> Result<String> {
let issuer = issuer.trim_end_matches('/');
let url = url::Url::parse(issuer)?;
if url.scheme() != "https" && url.scheme() != "http" {
return Err(Error::InvalidParam("issuer must use http or https scheme"));
}
Ok(format!("{}/.well-known/openid-configuration", issuer))
}
fn validate_metadata(metadata: &OidcProviderMetadata, expected_issuer: &str) -> Result<()> {
let normalized_expected = expected_issuer.trim_end_matches('/');
let normalized_actual = metadata.issuer.trim_end_matches('/');
if normalized_actual != normalized_expected {
return Err(Error::Discovery(format!(
"Issuer mismatch: expected '{}', got '{}'",
normalized_expected, normalized_actual
)));
}
if metadata.authorization_endpoint.is_empty() {
return Err(Error::Discovery("authorization_endpoint is missing".to_string()));
}
if metadata.token_endpoint.is_empty() {
return Err(Error::Discovery("token_endpoint is missing".to_string()));
}
if metadata.jwks_uri.is_empty() {
return Err(Error::Discovery("jwks_uri is missing".to_string()));
}
Ok(())
}
#[tracing::instrument(
name = "oidc_discover_tenant_legacy",
skip(http, cache),
fields(
issuer = %issuer,
tenant = ?tenant_config.tenant,
mode = ?tenant_config.mode
)
)]
pub async fn discover_with_tenant(
issuer: &str,
tenant_config: &TenantConfig,
http: &dyn HttpClientWithAdminSupport,
cache: &dyn Cache<String, OidcProviderMetadata>,
) -> Result<OidcProviderMetadata> {
let tenant_resolution = TenantResolution {
client_id_tenant: match tenant_config.mode {
crate::tenant::TenantMode::ClientId => tenant_config.tenant.clone(),
_ => None,
},
admin_override_tenant: None,
default_tenant: match tenant_config.mode {
crate::tenant::TenantMode::Single |
crate::tenant::TenantMode::QueryParam => tenant_config.tenant.clone(),
_ => None,
},
};
discover_with_tenant_resolution(issuer, &tenant_resolution, http, cache).await
}
pub async fn discover_with_tenant_simple(
issuer: &str,
tenant: Option<String>,
http: &dyn HttpClientWithAdminSupport,
cache: &dyn Cache<String, OidcProviderMetadata>,
) -> Result<OidcProviderMetadata> {
let tenant_resolution = TenantResolution {
client_id_tenant: None,
admin_override_tenant: None,
default_tenant: tenant,
};
discover_with_tenant_resolution(issuer, &tenant_resolution, http, cache).await
}