Skip to main content

authkestra_engine/auth/
discovery.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::AuthError;
4
5/// Metadata for an OpenID Connect provider.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ProviderMetadata {
8    /// The issuer URL
9    pub issuer: String,
10    /// The authorization endpoint URL
11    pub authorization_endpoint: String,
12    /// The token endpoint URL
13    pub token_endpoint: String,
14    /// The JWKS URI
15    pub jwks_uri: String,
16    /// The userinfo endpoint URL, if available
17    pub userinfo_endpoint: Option<String>,
18    /// Scopes supported by the provider
19    pub scopes_supported: Option<Vec<String>>,
20    /// Response types supported by the provider
21    pub response_types_supported: Option<Vec<String>>,
22    /// ID token signing algorithms supported by the provider
23    pub id_token_signing_alg_values_supported: Option<Vec<String>>,
24}
25
26impl ProviderMetadata {
27    /// Fetches metadata from the issuer URL (appends /.well-known/openid-configuration).
28    /// Also returns the parsed max-age from the Cache-Control header if present.
29    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}