1use crate::types::{AuthContext, AuthProvider, AuthType};
2use anyhow::{Result, anyhow};
3use base64::Engine;
4use chrono::{DateTime, TimeDelta, Utc};
5use jsonwebtoken::{Algorithm, Validation, decode, decode_header};
6use moka::future::Cache;
7use openidconnect::core::{CoreJsonWebKeySet, CoreProviderMetadata};
8use openidconnect::{IssuerUrl, JsonWebKey};
9use rsa::pkcs1::EncodeRsaPublicKey;
10use rsa::{BigUint, RsaPublicKey};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14use std::time::Duration;
15
16pub fn create_http_client() -> Result<reqwest::Client> {
43 reqwest::ClientBuilder::new()
44 .redirect(reqwest::redirect::Policy::none())
45 .build()
46 .map_err(|e| anyhow!("Failed to create HTTP client: {e:?}"))
47}
48
49async fn fetch_jwks(issuer_url: &IssuerUrl) -> Result<Arc<CoreJsonWebKeySet>> {
51 let http_client = create_http_client()?;
52
53 let metadata = CoreProviderMetadata::discover_async(issuer_url.clone(), &http_client)
55 .await
56 .map_err(|e| {
57 anyhow!(
58 "Failed to discover OIDC metadata from {}: {e:?}",
59 issuer_url
60 )
61 })?;
62
63 let jwks_uri = metadata.jwks_uri();
65 let jwks: CoreJsonWebKeySet = http_client
66 .get(jwks_uri.url().as_str())
67 .send()
68 .await
69 .map_err(|e| anyhow!("Failed to fetch JWKS from {}: {e:?}", jwks_uri))?
70 .json()
71 .await
72 .map_err(|e| anyhow!("Failed to parse JWKS: {e:?}"))?;
73
74 Ok(Arc::new(jwks))
75}
76
77struct JwksCache {
82 issuer_url: IssuerUrl,
83 cache: Cache<String, Arc<CoreJsonWebKeySet>>,
84}
85
86impl JwksCache {
87 fn new(issuer_url: IssuerUrl, ttl: Duration) -> Self {
89 let cache = Cache::builder().time_to_live(ttl).build();
90
91 Self { issuer_url, cache }
92 }
93
94 async fn get(&self) -> Result<Arc<CoreJsonWebKeySet>> {
96 let issuer_url = self.issuer_url.clone();
97
98 self.cache
99 .try_get_with(
100 "jwks".to_string(),
101 async move { fetch_jwks(&issuer_url).await },
102 )
103 .await
104 .map_err(|e| anyhow!("Failed to fetch JWKS: {e:?}"))
105 }
106}
107
108#[derive(Debug, Clone, Deserialize)]
110pub struct OidcIssuer {
111 pub issuer: String,
113 pub audience: String,
117}
118
119const DEFAULT_JWKS_REFRESH_INTERVAL_SECS: u64 = 3600;
120const DEFAULT_TOKEN_CACHE_SIZE: u64 = 1000;
121const DEFAULT_TOKEN_CACHE_TTL_SECS: u64 = 300;
122
123#[derive(Debug, Clone, Deserialize)]
125#[serde(default)]
126pub struct OidcConfig {
127 pub issuers: Vec<OidcIssuer>,
129 pub jwks_refresh_interval_secs: u64,
131 pub token_cache_size: u64,
133 pub token_cache_ttl_secs: u64,
135}
136
137impl Default for OidcConfig {
138 fn default() -> Self {
139 Self {
140 issuers: Vec::new(),
141 jwks_refresh_interval_secs: DEFAULT_JWKS_REFRESH_INTERVAL_SECS,
142 token_cache_size: DEFAULT_TOKEN_CACHE_SIZE,
143 token_cache_ttl_secs: DEFAULT_TOKEN_CACHE_TTL_SECS,
144 }
145 }
146}
147
148impl OidcConfig {
149 pub fn from_env() -> Result<Self> {
151 Self::from_env_var("MICROMEGAS_OIDC_CONFIG")
152 }
153
154 pub fn from_env_var(name: &str) -> Result<Self> {
156 let json =
157 std::env::var(name).map_err(|_| anyhow!("{name} environment variable not set"))?;
158 let config: OidcConfig =
159 serde_json::from_str(&json).map_err(|e| anyhow!("Failed to parse {name}: {e:?}"))?;
160 Ok(config)
161 }
162}
163
164#[derive(Debug, Serialize, Deserialize)]
166#[serde(untagged)]
167enum Audience {
168 Single(String),
169 Multiple(Vec<String>),
170}
171
172impl Audience {
173 fn contains(&self, aud: &str) -> bool {
174 match self {
175 Audience::Single(s) => s == aud,
176 Audience::Multiple(v) => v.iter().any(|a| a == aud),
177 }
178 }
179}
180
181#[derive(Debug, Serialize, Deserialize)]
194struct Claims {
195 iss: String,
197 sub: String,
199 aud: Audience,
202 exp: i64,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 email: Option<String>,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 verified_primary_email: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 preferred_username: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 upn: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none")]
218 unique_name: Option<String>,
219 #[serde(rename = "https://micromegas.io/email")]
221 #[serde(skip_serializing_if = "Option::is_none")]
222 namespaced_email: Option<String>,
223 #[serde(rename = "https://micromegas.io/name")]
225 #[serde(skip_serializing_if = "Option::is_none")]
226 namespaced_name: Option<String>,
227}
228
229impl Claims {
230 fn get_email(&self) -> Option<String> {
233 self.verified_primary_email
234 .clone()
235 .or_else(|| self.email.clone())
236 .or_else(|| self.namespaced_email.clone())
237 .or_else(|| self.preferred_username.clone())
238 .or_else(|| self.upn.clone())
239 .or_else(|| self.unique_name.clone())
240 }
241}
242
243struct OidcIssuerClient {
245 issuer: String,
246 audience: String,
247 jwks_cache: JwksCache,
248}
249
250impl OidcIssuerClient {
251 fn new(issuer: String, audience: String, jwks_ttl: Duration) -> Result<Self> {
252 let issuer_url = IssuerUrl::new(issuer.clone())
253 .map_err(|e| anyhow!("Invalid issuer URL '{}': {e:?}", issuer))?;
254
255 Ok(Self {
256 issuer,
257 audience,
258 jwks_cache: JwksCache::new(issuer_url, jwks_ttl),
259 })
260 }
261}
262
263fn jwk_to_decoding_key(
265 jwk: &openidconnect::core::CoreJsonWebKey,
266) -> Result<jsonwebtoken::DecodingKey> {
267 let jwk_json =
269 serde_json::to_value(jwk).map_err(|e| anyhow!("Failed to serialize JWK: {e:?}"))?;
270
271 let n = jwk_json
273 .get("n")
274 .and_then(|v| v.as_str())
275 .ok_or_else(|| anyhow!("JWK missing 'n' parameter"))?;
276 let e = jwk_json
277 .get("e")
278 .and_then(|v| v.as_str())
279 .ok_or_else(|| anyhow!("JWK missing 'e' parameter"))?;
280
281 let n_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
283 .decode(n.as_bytes())
284 .map_err(|e| anyhow!("Failed to decode 'n': {e:?}"))?;
285 let e_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
286 .decode(e.as_bytes())
287 .map_err(|e| anyhow!("Failed to decode 'e': {e:?}"))?;
288
289 let n_bigint = BigUint::from_bytes_be(&n_bytes);
291 let e_bigint = BigUint::from_bytes_be(&e_bytes);
292
293 let public_key = RsaPublicKey::new(n_bigint, e_bigint)
294 .map_err(|e| anyhow!("Failed to create RSA public key: {e:?}"))?;
295
296 let pem = public_key
298 .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
299 .map_err(|e| anyhow!("Failed to encode public key as PEM: {e:?}"))?;
300
301 jsonwebtoken::DecodingKey::from_rsa_pem(pem.as_bytes())
303 .map_err(|e| anyhow!("Failed to create decoding key: {e:?}"))
304}
305
306pub struct OidcAuthProvider {
311 clients: HashMap<String, Vec<Arc<OidcIssuerClient>>>,
313 token_cache: Cache<String, Arc<AuthContext>>,
315}
316
317impl std::fmt::Debug for OidcAuthProvider {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 f.debug_struct("OidcAuthProvider")
320 .field("num_clients", &self.clients.len())
321 .finish()
322 }
323}
324
325impl OidcAuthProvider {
326 pub async fn new(config: OidcConfig) -> Result<Self> {
330 if config.issuers.is_empty() {
331 return Err(anyhow!("At least one OIDC issuer must be configured"));
332 }
333
334 micromegas_tracing::info!("Configuring OIDC with {} issuer(s)", config.issuers.len());
335 for (idx, issuer_config) in config.issuers.iter().enumerate() {
336 micromegas_tracing::info!(
337 " Issuer {}: {} (audience: {})",
338 idx + 1,
339 issuer_config.issuer,
340 issuer_config.audience
341 );
342 }
343
344 let jwks_ttl = Duration::from_secs(config.jwks_refresh_interval_secs);
345 let mut clients: HashMap<String, Vec<Arc<OidcIssuerClient>>> = HashMap::new();
346
347 for issuer_config in config.issuers {
350 let client = OidcIssuerClient::new(
351 issuer_config.issuer.clone(),
352 issuer_config.audience,
353 jwks_ttl,
354 )?;
355
356 clients
357 .entry(issuer_config.issuer)
358 .or_default()
359 .push(Arc::new(client));
360 }
361
362 let token_cache = Cache::builder()
364 .max_capacity(config.token_cache_size)
365 .time_to_live(Duration::from_secs(config.token_cache_ttl_secs))
366 .build();
367
368 Ok(Self {
369 clients,
370 token_cache,
371 })
372 }
373
374 fn decode_payload_unsafe(&self, token: &str) -> Result<Claims> {
379 let parts: Vec<&str> = token.split('.').collect();
380 if parts.len() != 3 {
381 return Err(anyhow!("Invalid JWT format"));
382 }
383
384 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
386 .decode(parts[1].as_bytes())
387 .map_err(|e| anyhow!("Failed to decode JWT payload: {e:?}"))?;
388
389 let claims: Claims = serde_json::from_slice(&payload_bytes)
390 .map_err(|e| anyhow!("Failed to parse JWT claims: {e:?}"))?;
391
392 Ok(claims)
393 }
394
395 async fn validate_jwt_token(&self, token: &str) -> Result<AuthContext> {
407 let header = decode_header(token).map_err(|e| anyhow!("Invalid JWT header: {e:?}"))?;
409
410 let kid = header.kid;
412
413 let unverified_claims = self.decode_payload_unsafe(token)?;
415
416 let issuer_clients = self
418 .clients
419 .get(&unverified_claims.iss)
420 .ok_or_else(|| anyhow!("Unknown issuer: {}", unverified_claims.iss))?;
421
422 let first_client = issuer_clients
425 .first()
426 .ok_or_else(|| anyhow!("No clients configured for issuer"))?;
427
428 let jwks = first_client
429 .jwks_cache
430 .get()
431 .await
432 .map_err(|e| anyhow!("Failed to fetch JWKS: {e:?}"))?;
433
434 let keys_to_try: Vec<_> = if let Some(ref kid_value) = kid {
437 jwks.keys()
439 .iter()
440 .filter(|k| k.key_id().map(|id| id.as_str()) == Some(kid_value.as_str()))
441 .collect()
442 } else {
443 jwks.keys().iter().collect()
445 };
446
447 if keys_to_try.is_empty() {
448 return Err(if let Some(kid_value) = kid {
449 anyhow!("Key with kid '{}' not found in JWKS", kid_value)
450 } else {
451 anyhow!("No keys found in JWKS")
452 });
453 }
454
455 let mut key_error = anyhow!("No valid key found");
457 for key in keys_to_try {
458 match jwk_to_decoding_key(key) {
459 Ok(decoding_key) => {
460 match self
461 .try_validate_with_key(token, &decoding_key, issuer_clients)
462 .await
463 {
464 Ok(auth_ctx) => return Ok(auth_ctx),
465 Err(e) => key_error = e,
466 }
467 }
468 Err(e) => key_error = e,
469 }
470 }
471
472 Err(key_error)
473 }
474
475 async fn try_validate_with_key(
477 &self,
478 token: &str,
479 decoding_key: &jsonwebtoken::DecodingKey,
480 issuer_clients: &[Arc<OidcIssuerClient>],
481 ) -> Result<AuthContext> {
482 let configured_audiences: Vec<String> =
483 issuer_clients.iter().map(|c| c.audience.clone()).collect();
484 let mut last_error = anyhow!("No matching audience found");
485
486 for client in issuer_clients {
487 let mut validation = Validation::new(Algorithm::RS256);
489 validation.validate_aud = false;
491 validation.set_issuer(&[&client.issuer]);
492
493 let claims = match decode::<Claims>(token, decoding_key, &validation) {
494 Ok(token_data) => token_data.claims,
495 Err(e) => {
496 last_error = anyhow!("Token validation failed: {e:?}");
497 continue;
498 }
499 };
500
501 if claims.aud.contains(&client.audience) {
503 let expires_at = DateTime::from_timestamp(claims.exp, 0)
505 .ok_or_else(|| anyhow!("Invalid expiration timestamp"))?;
506
507 if expires_at < Utc::now() {
508 return Err(anyhow!("Token has expired"));
509 }
510
511 let email = claims.get_email();
512
513 return Ok(AuthContext {
514 subject: claims.sub,
515 email,
516 issuer: claims.iss,
517 audience: Some(client.audience.clone()),
518 expires_at: Some(expires_at),
519 auth_type: AuthType::Oidc,
520 allow_delegation: false,
521 bound_audience: None,
524 read_audiences: vec![],
525 memberships: Arc::from([]),
528 });
529 } else {
530 let actual_audiences = match &claims.aud {
532 Audience::Single(s) => vec![s.clone()],
533 Audience::Multiple(v) => v.clone(),
534 };
535 last_error = anyhow!(
536 "Token audience mismatch - configured audiences: {:?}, token audiences: {:?}",
537 configured_audiences,
538 actual_audiences
539 );
540 }
541 }
542
543 Err(last_error)
545 }
546}
547
548#[async_trait::async_trait]
549impl AuthProvider for OidcAuthProvider {
550 async fn validate_request(
551 &self,
552 parts: &dyn crate::types::RequestParts,
553 ) -> Result<AuthContext> {
554 let token = parts
555 .bearer_token()
556 .ok_or_else(|| anyhow!("missing bearer token"))?;
557
558 if let Some(cached) = self.token_cache.get(token).await {
561 let is_expired = cached
562 .expires_at
563 .map(|exp| exp <= Utc::now() + TimeDelta::seconds(30))
564 .unwrap_or(false);
565
566 if is_expired {
567 self.token_cache.remove(token).await;
568 } else {
569 return Ok((*cached).clone());
570 }
571 }
572
573 let auth_ctx = self.validate_jwt_token(token).await?;
575
576 self.token_cache
578 .insert(token.to_string(), Arc::new(auth_ctx.clone()))
579 .await;
580
581 Ok(auth_ctx)
582 }
583}