fraiseql_core/security/oidc/
jwks.rs1use std::time::{Duration, Instant};
7
8use jsonwebtoken::DecodingKey;
9use serde::Deserialize;
10
11use crate::security::{
12 errors::{Result, SecurityError},
13 oidc::token::OidcValidator,
14};
15
16pub const MAX_JWKS_RESPONSE_BYTES: usize = 1024 * 1024; #[derive(Debug, Clone, Deserialize)]
31pub struct OidcDiscoveryDocument {
32 pub issuer: String,
34
35 pub jwks_uri: String,
37
38 #[serde(default)]
40 pub id_token_signing_alg_values_supported: Vec<String>,
41
42 #[serde(default)]
44 pub authorization_endpoint: Option<String>,
45
46 #[serde(default)]
48 pub token_endpoint: Option<String>,
49}
50
51#[derive(Debug, Clone, Deserialize)]
57pub struct Jwks {
58 pub keys: Vec<Jwk>,
60}
61
62#[derive(Debug, Clone, Deserialize)]
64pub struct Jwk {
65 pub kty: String,
67
68 pub kid: Option<String>,
70
71 #[serde(default)]
73 pub alg: Option<String>,
74
75 #[serde(rename = "use")]
77 pub key_use: Option<String>,
78
79 pub n: Option<String>,
81
82 pub e: Option<String>,
84
85 #[serde(default)]
87 pub x5c: Vec<String>,
88}
89
90#[derive(Debug)]
92pub struct CachedJwks {
93 pub(super) jwks: Jwks,
94 pub(super) fetched_at: Instant,
95 pub(super) ttl: Duration,
96}
97
98impl CachedJwks {
99 pub(super) fn is_expired(&self) -> bool {
100 self.fetched_at.elapsed() > self.ttl
101 }
102}
103
104impl OidcValidator {
109 pub(super) async fn get_decoding_key(&self, kid: &str) -> Result<DecodingKey> {
117 {
119 let cache = self.jwks_cache.read();
120 if let Some(ref cached) = *cache {
121 if !cached.is_expired() {
122 if let Some(key) = self.find_key(&cached.jwks, kid) {
123 return self.jwk_to_decoding_key(key);
124 }
125 }
126 }
127 }
128
129 let jwks = self.fetch_jwks().await?;
131
132 if self.detect_key_rotation(&jwks) {
134 tracing::warn!(
135 "OIDC key rotation detected: some previously cached keys no longer available"
136 );
137 }
138
139 let key_index =
141 jwks.keys.iter().position(|k| k.kid.as_deref() == Some(kid)).ok_or_else(|| {
142 tracing::debug!(kid = %kid, "Key not found in JWKS");
143 SecurityError::InvalidToken
144 })?;
145
146 let key = jwks.keys[key_index].clone();
148
149 {
151 let mut cache = self.jwks_cache.write();
152 *cache = Some(CachedJwks {
153 jwks,
154 fetched_at: Instant::now(),
155 ttl: Duration::from_secs(self.config.jwks_cache_ttl_secs),
156 });
157 }
158
159 self.jwk_to_decoding_key(&key)
160 }
161
162 async fn fetch_jwks(&self) -> Result<Jwks> {
169 tracing::debug!(uri = %self.jwks_uri, "Fetching JWKS");
170
171 let response = self.http_client.get(&self.jwks_uri).send().await.map_err(|e| {
172 tracing::error!(error = %e, "Failed to fetch JWKS");
173 SecurityError::SecurityConfigError(format!("Failed to fetch JWKS: {e}"))
174 })?;
175
176 if !response.status().is_success() {
177 return Err(SecurityError::SecurityConfigError(format!(
178 "JWKS fetch failed with status: {}",
179 response.status()
180 )));
181 }
182
183 let body_bytes = response.bytes().await.map_err(|e| {
186 SecurityError::SecurityConfigError(format!("Failed to read JWKS response body: {e}"))
187 })?;
188
189 if body_bytes.len() > MAX_JWKS_RESPONSE_BYTES {
190 return Err(SecurityError::SecurityConfigError(format!(
191 "JWKS response body too large ({} bytes, max {MAX_JWKS_RESPONSE_BYTES})",
192 body_bytes.len()
193 )));
194 }
195
196 let jwks: Jwks = serde_json::from_slice(&body_bytes).map_err(|e| {
197 SecurityError::SecurityConfigError(format!("Invalid JWKS response: {e}"))
198 })?;
199
200 tracing::debug!(key_count = jwks.keys.len(), "JWKS fetched successfully");
201
202 Ok(jwks)
203 }
204
205 pub(super) fn find_key<'a>(&self, jwks: &'a Jwks, kid: &str) -> Option<&'a Jwk> {
207 jwks.keys.iter().find(|k| k.kid.as_deref() == Some(kid))
208 }
209
210 pub(super) fn detect_key_rotation(&self, new_jwks: &Jwks) -> bool {
215 let cache = self.jwks_cache.read();
216 if let Some(ref cached) = *cache {
217 let old_kids: std::collections::HashSet<_> =
219 cached.jwks.keys.iter().filter_map(|k| k.kid.as_deref()).collect();
220
221 let new_kids: std::collections::HashSet<_> =
223 new_jwks.keys.iter().filter_map(|k| k.kid.as_deref()).collect();
224
225 !old_kids.is_subset(&new_kids)
227 } else {
228 false
229 }
230 }
231
232 pub(super) fn jwk_to_decoding_key(&self, jwk: &Jwk) -> Result<DecodingKey> {
239 match jwk.kty.as_str() {
240 "RSA" => {
241 let n = jwk.n.as_ref().ok_or(SecurityError::InvalidToken)?;
242 let e = jwk.e.as_ref().ok_or(SecurityError::InvalidToken)?;
243
244 DecodingKey::from_rsa_components(n, e).map_err(|e| {
245 tracing::debug!(error = %e, "Failed to create RSA decoding key");
246 SecurityError::InvalidToken
247 })
248 },
249 other => {
250 tracing::debug!(key_type = %other, "Unsupported key type");
251 Err(SecurityError::InvalidTokenAlgorithm {
252 algorithm: other.to_string(),
253 })
254 },
255 }
256 }
257}