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