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 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 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 pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
61 self.audiences.push(audience.into());
62 self
63 }
64
65 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 pub fn with_preferred_algorithms(mut self, algorithms: Vec<String>) -> Self {
77 self.preferred_algorithms = Some(algorithms);
78 self
79 }
80
81 pub fn with_timeout(mut self, timeout: Duration) -> Self {
83 self.timeout = timeout;
84 self
85 }
86
87 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 let jwks_uri = metadata["jwks_uri"]
113 .as_str()
114 .ok_or_else(|| ErrorUnauthorized("Missing jwks_uri in OIDC discovery"))?;
115
116 let jwks_url = Url::parse(jwks_uri)
118 .map_err(|e| ErrorUnauthorized(format!("Invalid JWKS URI: {}", e)))?;
119
120 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()]); let algorithms_to_use = if let Some(preferred) = self.preferred_algorithms {
132 preferred
134 .into_iter()
135 .filter(|alg| provider_algorithms.contains(alg))
136 .collect::<Vec<String>>()
137 } else {
138 provider_algorithms.clone()
140 };
141
142 let final_algorithms = if algorithms_to_use.is_empty() {
144 vec!["RS256".to_string()]
145 } else {
146 algorithms_to_use
147 };
148
149 info!(
151 "Using OIDC token signing algorithms: {:?}",
152 final_algorithms
153 );
154
155 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 let jwks_client = JwksClient::builder().build(source);
166
167 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 pub fn builder() -> OIDCValidatorBuilder {
183 OIDCValidatorBuilder::new()
184 }
185
186 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 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 pub fn audiences(&self) -> &[String] {
214 &self.audiences
215 }
216
217 pub fn issuer(&self) -> &str {
219 &self.issuer
220 }
221
222 pub fn supported_algorithms(&self) -> &[String] {
224 &self.supported_algorithms
225 }
226
227 pub fn userinfo_endpoint(&self) -> Option<&str> {
229 self.userinfo_endpoint.as_deref()
230 }
231
232 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 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 let token_data: Result<Claims, JwksClientError> =
280 jwks_client.decode::<Claims>(&token, &audiences).await;
281
282 match token_data {
283 Ok(claims) => {
284 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 Err(ErrorUnauthorized(format!("Invalid OIDC token: {}", e)))
296 }
297 }
298 })
299 }
300}