use securitydept_utils::observability::{
AuthFlowDiagnosis, AuthFlowDiagnosisField, AuthFlowOperation, DiagnosedResult,
};
use super::{
capabilities::FrontendOidcModeCapabilities, config::ResolvedFrontendOidcModeConfig,
contracts::FrontendOidcModeConfigProjection,
};
#[derive(Debug, Clone)]
pub struct FrontendOidcModeRuntime {
config: ResolvedFrontendOidcModeConfig,
}
impl FrontendOidcModeRuntime {
pub fn new(config: ResolvedFrontendOidcModeConfig) -> Self {
config.capabilities.warn_unsafe();
Self { config }
}
pub fn config(&self) -> &ResolvedFrontendOidcModeConfig {
&self.config
}
pub fn capabilities(&self) -> &FrontendOidcModeCapabilities {
&self.config.capabilities
}
pub async fn config_projection(&self) -> std::io::Result<FrontendOidcModeConfigProjection> {
self.config_projection_with_diagnosis().await.into_result()
}
pub async fn config_projection_with_diagnosis(
&self,
) -> DiagnosedResult<FrontendOidcModeConfigProjection, std::io::Error> {
let base_diagnosis = AuthFlowDiagnosis::started(AuthFlowOperation::PROJECTION_CONFIG_FETCH)
.field(AuthFlowDiagnosisField::MODE, "frontend_oidc")
.field("client_id", self.config.oidc_client.client_id.clone())
.field("pkce_enabled", self.config.oidc_client.pkce_enabled)
.field(
"claims_check_script_configured",
self.config.oidc_client.claims_check_script.is_some(),
);
match self.config.to_config_projection().await {
Ok(projection) => DiagnosedResult::success(
base_diagnosis
.with_outcome(
securitydept_utils::observability::AuthFlowDiagnosisOutcome::Succeeded,
)
.field("has_client_secret", projection.client_secret.is_some())
.field(
"has_claims_check_script",
projection.claims_check_script.is_some(),
),
projection,
),
Err(error) => DiagnosedResult::failure(
base_diagnosis
.with_outcome(
securitydept_utils::observability::AuthFlowDiagnosisOutcome::Failed,
)
.field(
AuthFlowDiagnosisField::FAILURE_STAGE,
"projection_generation",
),
error,
),
}
}
}
#[cfg(test)]
mod tests {
use securitydept_oauth_provider::{OAuthProviderRemoteConfig, OidcSharedConfig};
use securitydept_utils::secret::SecretString;
use super::*;
use crate::frontend_oidc_mode::{
capabilities::UnsafeFrontendClientSecret,
config::{FrontendOidcModeConfig, FrontendOidcModeConfigSource},
};
fn test_runtime() -> FrontendOidcModeRuntime {
let shared = OidcSharedConfig {
remote: OAuthProviderRemoteConfig {
well_known_url: Some(
"https://auth.example.com/.well-known/openid-configuration".to_string(),
),
..Default::default()
},
client_id: Some("spa-client".to_string()),
..Default::default()
};
let config = FrontendOidcModeConfig::default()
.resolve_all(&shared)
.expect("should resolve");
FrontendOidcModeRuntime::new(config)
}
#[tokio::test]
async fn runtime_produces_config_projection() {
let runtime = test_runtime();
let projection = runtime
.config_projection()
.await
.expect("projection should succeed");
assert_eq!(projection.client_id, "spa-client");
assert_eq!(
projection.well_known_url.as_deref(),
Some("https://auth.example.com/.well-known/openid-configuration")
);
assert!(projection.client_secret.is_none());
}
#[tokio::test]
async fn runtime_exposes_client_secret_when_capability_enabled() {
let shared = OidcSharedConfig {
remote: OAuthProviderRemoteConfig {
well_known_url: Some(
"https://auth.example.com/.well-known/openid-configuration".to_string(),
),
..Default::default()
},
client_id: Some("spa-client".to_string()),
..Default::default()
};
let config = FrontendOidcModeConfig {
oidc_client: securitydept_oidc_client::OidcClientRawConfig {
client_secret: Some(SecretString::from("test-secret")),
..Default::default()
},
capabilities: FrontendOidcModeCapabilities {
unsafe_frontend_client_secret: UnsafeFrontendClientSecret::Enabled,
},
};
let resolved = config.resolve_all(&shared).expect("should resolve");
let runtime = FrontendOidcModeRuntime::new(resolved);
let projection = runtime
.config_projection()
.await
.expect("projection should succeed");
assert_eq!(projection.client_secret.as_deref(), Some("test-secret"));
}
#[tokio::test]
async fn runtime_reports_projection_diagnosis() {
let runtime = test_runtime();
let diagnosed = runtime.config_projection_with_diagnosis().await;
assert!(diagnosed.result().is_ok());
assert_eq!(
diagnosed.diagnosis().operation,
AuthFlowOperation::PROJECTION_CONFIG_FETCH
);
assert_eq!(diagnosed.diagnosis().outcome.as_str(), "succeeded");
assert_eq!(
diagnosed.diagnosis().fields[AuthFlowDiagnosisField::MODE],
"frontend_oidc"
);
assert_eq!(diagnosed.diagnosis().fields["client_id"], "spa-client");
}
}