authkestra_engine/auth/
discovery.rs1use serde::{Deserialize, Serialize};
2
3use crate::error::AuthError;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ProviderMetadata {
8 pub issuer: String,
10 pub authorization_endpoint: String,
12 pub token_endpoint: String,
14 pub jwks_uri: String,
16 pub userinfo_endpoint: Option<String>,
18 pub scopes_supported: Option<Vec<String>>,
20 pub response_types_supported: Option<Vec<String>>,
22 pub id_token_signing_alg_values_supported: Option<Vec<String>>,
24}
25
26impl ProviderMetadata {
27 pub async fn discover(
30 issuer_url: &str,
31 client: reqwest::Client,
32 ) -> Result<(Self, Option<std::time::Duration>), AuthError> {
33 let mut url = url::Url::parse(issuer_url)
34 .map_err(|e| AuthError::Discovery(format!("Invalid issuer URL: {e}")))?;
35
36 if !url.path().ends_with("/.well-known/openid-configuration") {
37 let mut path = url.path_segments_mut().unwrap();
38 path.push(".well-known");
39 path.push("openid-configuration");
40 }
41
42 let response = client
43 .get(url)
44 .send()
45 .await
46 .map_err(|_| AuthError::Network)?;
47
48 let mut cache_max_age = None;
49 if let Some(cache_control) = response.headers().get(reqwest::header::CACHE_CONTROL) {
50 if let Ok(cc_str) = cache_control.to_str() {
51 for directive in cc_str.split(',') {
52 let directive = directive.trim();
53 if let Some(rest) = directive.strip_prefix("max-age=") {
54 if let Ok(secs) = rest.parse::<u64>() {
55 cache_max_age = Some(std::time::Duration::from_secs(secs));
56 }
57 }
58 }
59 }
60 }
61
62 let metadata = response
63 .json::<ProviderMetadata>()
64 .await
65 .map_err(|e| AuthError::Discovery(format!("Failed to parse metadata: {e}")))?;
66
67 Ok((metadata, cache_max_age))
68 }
69}