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)]
7#[non_exhaustive]
8pub struct ProviderMetadata {
9    /// The issuer URL
10    pub issuer: String,
11    /// The authorization endpoint URL
12    pub authorization_endpoint: String,
13    /// The token endpoint URL
14    pub token_endpoint: String,
15    /// The JWKS URI
16    pub jwks_uri: String,
17    /// The userinfo endpoint URL, if available
18    pub userinfo_endpoint: Option<String>,
19    /// Scopes supported by the provider
20    pub scopes_supported: Option<Vec<String>>,
21    /// Response types supported by the provider
22    pub response_types_supported: Option<Vec<String>>,
23    /// ID token signing algorithms supported by the provider
24    pub id_token_signing_alg_values_supported: Option<Vec<String>>,
25}
26
27impl ProviderMetadata {
28    /// Fetches metadata from the issuer URL (appends /.well-known/openid-configuration).
29    /// Also returns the parsed max-age from the Cache-Control header if present.
30    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}