Skip to main content

paas_server/auth/
oidc.rs

1use crate::auth::core::{AuthInfo, TokenValidator};
2use crate::auth::jwt::Claims;
3use actix_web::error::ErrorUnauthorized;
4use actix_web::Error;
5use jwks_client_rs::{source::WebSource, JwksClient, JwksClientError};
6use log::info;
7use reqwest::Url;
8use serde_json::Value;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::time::Duration;
13
14pub struct OIDCAuthConfig {
15    pub provider_url: String,
16    pub audiences: Vec<String>,
17    pub discovery_timeout: Duration,
18}
19
20#[derive(Clone)]
21pub struct OIDCValidator {
22    jwks_client: Arc<JwksClient<WebSource>>,
23    audiences: Vec<String>,
24    issuer: String,
25    supported_algorithms: Vec<String>,
26    userinfo_endpoint: Option<String>,
27}
28
29pub struct OIDCValidatorBuilder {
30    issuer_url: Option<String>,
31    audiences: Vec<String>,
32    timeout: Duration,
33    preferred_algorithms: Option<Vec<String>>,
34}
35
36impl Default for OIDCValidatorBuilder {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl OIDCValidatorBuilder {
43    /// Create a new OIDC validator builder with default values
44    pub fn new() -> Self {
45        Self {
46            issuer_url: None,
47            audiences: Vec::new(),
48            timeout: Duration::from_secs(10),
49            preferred_algorithms: None,
50        }
51    }
52
53    /// Set the issuer URL for OIDC discovery
54    pub fn with_issuer(mut self, issuer_url: impl Into<String>) -> Self {
55        self.issuer_url = Some(issuer_url.into());
56        self
57    }
58
59    /// Add an audience that the JWT must be intended for
60    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
61        self.audiences.push(audience.into());
62        self
63    }
64
65    /// Add multiple audiences that the JWT may be intended for
66    pub fn with_audiences(
67        mut self,
68        audiences: impl IntoIterator<Item = impl Into<String>>,
69    ) -> Self {
70        self.audiences
71            .extend(audiences.into_iter().map(|a| a.into()));
72        self
73    }
74
75    /// Set preferred signing algorithms in order of preference
76    pub fn with_preferred_algorithms(mut self, algorithms: Vec<String>) -> Self {
77        self.preferred_algorithms = Some(algorithms);
78        self
79    }
80
81    /// Set the timeout for OIDC discovery and JWKS requests
82    pub fn with_timeout(mut self, timeout: Duration) -> Self {
83        self.timeout = timeout;
84        self
85    }
86
87    /// Build the OIDCValidator instance
88    pub async fn build(self) -> Result<OIDCValidator, Error> {
89        let issuer_url = self
90            .issuer_url
91            .ok_or_else(|| ErrorUnauthorized("OIDC issuer URL is required"))?;
92
93        let client = reqwest::Client::new();
94        let discovery_url = format!(
95            "{}/.well-known/openid-configuration",
96            issuer_url.trim_end_matches('/')
97        );
98
99        let metadata = client
100            .get(&discovery_url)
101            .send()
102            .await
103            .map_err(|e| {
104                eprintln!("Failed to discover OIDC provider: {}", e);
105                ErrorUnauthorized(format!("Failed to discover OIDC provider: {}", e))
106            })?
107            .json::<Value>()
108            .await
109            .map_err(|e| ErrorUnauthorized(format!("Invalid OIDC discovery response: {}", e)))?;
110
111        // Extract JWKS URI from metadata
112        let jwks_uri = metadata["jwks_uri"]
113            .as_str()
114            .ok_or_else(|| ErrorUnauthorized("Missing jwks_uri in OIDC discovery"))?;
115
116        // Parse and validate the JWKS URL
117        let jwks_url = Url::parse(jwks_uri)
118            .map_err(|e| ErrorUnauthorized(format!("Invalid JWKS URI: {}", e)))?;
119
120        // Get supported algorithms from metadata
121        let provider_algorithms = metadata["id_token_signing_alg_values_supported"]
122            .as_array()
123            .map(|algs| {
124                algs.iter()
125                    .filter_map(|alg| alg.as_str().map(String::from))
126                    .collect::<Vec<String>>()
127            })
128            .unwrap_or_else(|| vec!["RS256".to_string()]); // Default to RS256 if not specified
129
130        // Determine which algorithms to use (preferred or discovered)
131        let algorithms_to_use = if let Some(preferred) = self.preferred_algorithms {
132            // Filter preferred algorithms to only include those supported by the provider
133            preferred
134                .into_iter()
135                .filter(|alg| provider_algorithms.contains(alg))
136                .collect::<Vec<String>>()
137        } else {
138            // Use provider's algorithms
139            provider_algorithms.clone()
140        };
141
142        // If no algorithms match, use RS256 as a fallback
143        let final_algorithms = if algorithms_to_use.is_empty() {
144            vec!["RS256".to_string()]
145        } else {
146            algorithms_to_use
147        };
148
149        // Log the algorithms being used
150        info!(
151            "Using OIDC token signing algorithms: {:?}",
152            final_algorithms
153        );
154
155        // Configure the WebSource
156        let source = WebSource::builder()
157            .with_timeout(self.timeout)
158            .with_connect_timeout(self.timeout)
159            .build(jwks_url)
160            .map_err(|e| ErrorUnauthorized(format!("Failed to create WebSource: {}", e)))?;
161
162        // Build the JWKS client
163        // NOTE: jwks_client_rs doesn't have a direct way to specify algorithms,
164        // it will accept keys for various algorithms from the JWKS endpoint
165        let jwks_client = JwksClient::builder().build(source);
166
167        // Get userinfo endpoint for additional user info if needed
168        let userinfo_endpoint = metadata["userinfo_endpoint"].as_str().map(String::from);
169
170        Ok(OIDCValidator {
171            jwks_client: Arc::new(jwks_client),
172            audiences: self.audiences,
173            issuer: issuer_url,
174            supported_algorithms: final_algorithms,
175            userinfo_endpoint,
176        })
177    }
178}
179
180impl OIDCValidator {
181    /// Create a new builder for configuring the OIDC validator
182    pub fn builder() -> OIDCValidatorBuilder {
183        OIDCValidatorBuilder::new()
184    }
185
186    /// Create a simple OIDC validator with minimal configuration (for backward compatibility)
187    pub async fn new(
188        issuer_url: &str,
189        audiences: Vec<&str>,
190        timeout: Duration,
191    ) -> Result<Self, Error> {
192        let mut builder = Self::builder()
193            .with_issuer(issuer_url)
194            .with_timeout(timeout);
195
196        for audience in audiences {
197            builder = builder.with_audience(audience);
198        }
199
200        builder.build().await
201    }
202
203    /// Create an OIDC validator that accepts the specified issuer
204    pub async fn new_with_discovery(issuer_url: &str, timeout: Duration) -> Result<Self, Error> {
205        Self::builder()
206            .with_issuer(issuer_url)
207            .with_timeout(timeout)
208            .build()
209            .await
210    }
211
212    /// Get the currently configured audiences
213    pub fn audiences(&self) -> &[String] {
214        &self.audiences
215    }
216
217    /// Get the issuer URL
218    pub fn issuer(&self) -> &str {
219        &self.issuer
220    }
221
222    /// Get the supported algorithms
223    pub fn supported_algorithms(&self) -> &[String] {
224        &self.supported_algorithms
225    }
226
227    /// Get the userinfo endpoint if available
228    pub fn userinfo_endpoint(&self) -> Option<&str> {
229        self.userinfo_endpoint.as_deref()
230    }
231
232    /// Fetch additional user information from the userinfo endpoint
233    pub async fn fetch_userinfo(&self, access_token: &str) -> Result<Value, Error> {
234        let userinfo_url = self
235            .userinfo_endpoint
236            .as_ref()
237            .ok_or_else(|| ErrorUnauthorized("Userinfo endpoint not available"))?;
238
239        let client = reqwest::Client::new();
240        let response = client
241            .get(userinfo_url)
242            .bearer_auth(access_token)
243            .send()
244            .await
245            .map_err(|e| ErrorUnauthorized(format!("Failed to fetch userinfo: {}", e)))?;
246
247        if !response.status().is_success() {
248            return Err(ErrorUnauthorized(format!(
249                "Userinfo request failed with status: {}",
250                response.status()
251            )));
252        }
253
254        response
255            .json::<Value>()
256            .await
257            .map_err(|e| ErrorUnauthorized(format!("Failed to parse userinfo response: {}", e)))
258    }
259
260    /// Check if a specific algorithm is supported
261    pub fn supports_algorithm(&self, algorithm: &str) -> bool {
262        self.supported_algorithms.contains(&algorithm.to_string())
263    }
264}
265
266impl TokenValidator for OIDCValidator {
267    fn validate_token<'a>(
268        &'a self,
269        token: &'a str,
270    ) -> Pin<Box<dyn Future<Output = Result<AuthInfo, Error>> + Send + 'a>> {
271        let token = token.to_string();
272        let audiences = self.audiences.clone();
273        let jwks_client = self.jwks_client.clone();
274
275        Box::pin(async move {
276            // Decode and validate the token
277            // JwksClient internally fetches the JWK based on the token's header
278            // and validates using the appropriate algorithm
279            let token_data: Result<Claims, JwksClientError> =
280                jwks_client.decode::<Claims>(&token, &audiences).await;
281
282            match token_data {
283                Ok(claims) => {
284                    // Convert claims to AuthInfo
285                    // Use name if available, otherwise use sub
286                    let name = claims.name.clone().unwrap_or_else(|| claims.sub.clone());
287                    Ok(AuthInfo {
288                        name,
289                        sub: claims.sub,
290                        groups: claims.groups,
291                    })
292                }
293                Err(e) => {
294                    // Return specific error based on validation failure
295                    Err(ErrorUnauthorized(format!("Invalid OIDC token: {}", e)))
296                }
297            }
298        })
299    }
300}