this repo has no description
1use reqwest::Client; 2use serde::{Deserialize, Serialize}; 3use std::collections::HashMap; 4use std::sync::Arc; 5use tokio::sync::RwLock; 6 7use super::OAuthError; 8 9#[derive(Debug, Clone, Serialize, Deserialize)] 10pub struct ClientMetadata { 11 pub client_id: String, 12 #[serde(skip_serializing_if = "Option::is_none")] 13 pub client_name: Option<String>, 14 #[serde(skip_serializing_if = "Option::is_none")] 15 pub client_uri: Option<String>, 16 #[serde(skip_serializing_if = "Option::is_none")] 17 pub logo_uri: Option<String>, 18 pub redirect_uris: Vec<String>, 19 #[serde(default)] 20 pub grant_types: Vec<String>, 21 #[serde(default)] 22 pub response_types: Vec<String>, 23 #[serde(skip_serializing_if = "Option::is_none")] 24 pub scope: Option<String>, 25 #[serde(skip_serializing_if = "Option::is_none")] 26 pub token_endpoint_auth_method: Option<String>, 27 #[serde(skip_serializing_if = "Option::is_none")] 28 pub dpop_bound_access_tokens: Option<bool>, 29 #[serde(skip_serializing_if = "Option::is_none")] 30 pub jwks: Option<serde_json::Value>, 31 #[serde(skip_serializing_if = "Option::is_none")] 32 pub jwks_uri: Option<String>, 33 #[serde(skip_serializing_if = "Option::is_none")] 34 pub application_type: Option<String>, 35} 36 37impl Default for ClientMetadata { 38 fn default() -> Self { 39 Self { 40 client_id: String::new(), 41 client_name: None, 42 client_uri: None, 43 logo_uri: None, 44 redirect_uris: Vec::new(), 45 grant_types: vec!["authorization_code".to_string()], 46 response_types: vec!["code".to_string()], 47 scope: None, 48 token_endpoint_auth_method: Some("none".to_string()), 49 dpop_bound_access_tokens: None, 50 jwks: None, 51 jwks_uri: None, 52 application_type: None, 53 } 54 } 55} 56 57#[derive(Clone)] 58pub struct ClientMetadataCache { 59 cache: Arc<RwLock<HashMap<String, CachedMetadata>>>, 60 jwks_cache: Arc<RwLock<HashMap<String, CachedJwks>>>, 61 http_client: Client, 62 cache_ttl_secs: u64, 63} 64 65struct CachedMetadata { 66 metadata: ClientMetadata, 67 cached_at: std::time::Instant, 68} 69 70struct CachedJwks { 71 jwks: serde_json::Value, 72 cached_at: std::time::Instant, 73} 74 75impl ClientMetadataCache { 76 pub fn new(cache_ttl_secs: u64) -> Self { 77 Self { 78 cache: Arc::new(RwLock::new(HashMap::new())), 79 jwks_cache: Arc::new(RwLock::new(HashMap::new())), 80 http_client: Client::builder() 81 .timeout(std::time::Duration::from_secs(30)) 82 .connect_timeout(std::time::Duration::from_secs(10)) 83 .build() 84 .unwrap_or_else(|_| Client::new()), 85 cache_ttl_secs, 86 } 87 } 88 89 fn is_loopback_client(client_id: &str) -> bool { 90 if let Ok(url) = reqwest::Url::parse(client_id) { 91 url.scheme() == "http" 92 && matches!(url.host_str(), Some("localhost") | Some("127.0.0.1")) 93 } else { 94 false 95 } 96 } 97 98 fn build_loopback_metadata(client_id: &str) -> Result<ClientMetadata, OAuthError> { 99 let url = reqwest::Url::parse(client_id) 100 .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".to_string()))?; 101 let mut redirect_uris = Vec::new(); 102 for (key, value) in url.query_pairs() { 103 if key == "redirect_uri" { 104 redirect_uris.push(value.to_string()); 105 } 106 } 107 if redirect_uris.is_empty() { 108 redirect_uris.push("http://127.0.0.1/callback".to_string()); 109 redirect_uris.push("http://localhost/callback".to_string()); 110 } 111 let scope = Some("atproto transition:generic transition:chat.bsky".to_string()); 112 Ok(ClientMetadata { 113 client_id: client_id.to_string(), 114 client_name: Some("Loopback Client".to_string()), 115 client_uri: None, 116 logo_uri: None, 117 redirect_uris, 118 grant_types: vec![ 119 "authorization_code".to_string(), 120 "refresh_token".to_string(), 121 ], 122 response_types: vec!["code".to_string()], 123 scope, 124 token_endpoint_auth_method: Some("none".to_string()), 125 dpop_bound_access_tokens: Some(false), 126 jwks: None, 127 jwks_uri: None, 128 application_type: Some("native".to_string()), 129 }) 130 } 131 132 pub async fn get(&self, client_id: &str) -> Result<ClientMetadata, OAuthError> { 133 if Self::is_loopback_client(client_id) { 134 return Self::build_loopback_metadata(client_id); 135 } 136 { 137 let cache = self.cache.read().await; 138 if let Some(cached) = cache.get(client_id) 139 && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs 140 { 141 return Ok(cached.metadata.clone()); 142 } 143 } 144 let metadata = self.fetch_metadata(client_id).await?; 145 { 146 let mut cache = self.cache.write().await; 147 cache.insert( 148 client_id.to_string(), 149 CachedMetadata { 150 metadata: metadata.clone(), 151 cached_at: std::time::Instant::now(), 152 }, 153 ); 154 } 155 Ok(metadata) 156 } 157 158 pub async fn get_jwks( 159 &self, 160 metadata: &ClientMetadata, 161 ) -> Result<serde_json::Value, OAuthError> { 162 if let Some(jwks) = &metadata.jwks { 163 return Ok(jwks.clone()); 164 } 165 let jwks_uri = metadata.jwks_uri.as_ref().ok_or_else(|| { 166 OAuthError::InvalidClient( 167 "Client using private_key_jwt must have jwks or jwks_uri".to_string(), 168 ) 169 })?; 170 { 171 let cache = self.jwks_cache.read().await; 172 if let Some(cached) = cache.get(jwks_uri) 173 && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs 174 { 175 return Ok(cached.jwks.clone()); 176 } 177 } 178 let jwks = self.fetch_jwks(jwks_uri).await?; 179 { 180 let mut cache = self.jwks_cache.write().await; 181 cache.insert( 182 jwks_uri.clone(), 183 CachedJwks { 184 jwks: jwks.clone(), 185 cached_at: std::time::Instant::now(), 186 }, 187 ); 188 } 189 Ok(jwks) 190 } 191 192 async fn fetch_jwks(&self, jwks_uri: &str) -> Result<serde_json::Value, OAuthError> { 193 if !jwks_uri.starts_with("https://") 194 && (!jwks_uri.starts_with("http://") 195 || (!jwks_uri.contains("localhost") && !jwks_uri.contains("127.0.0.1"))) 196 { 197 return Err(OAuthError::InvalidClient( 198 "jwks_uri must use https (except for localhost)".to_string(), 199 )); 200 } 201 let response = self 202 .http_client 203 .get(jwks_uri) 204 .header("Accept", "application/json") 205 .send() 206 .await 207 .map_err(|e| { 208 OAuthError::InvalidClient(format!("Failed to fetch JWKS from {}: {}", jwks_uri, e)) 209 })?; 210 if !response.status().is_success() { 211 return Err(OAuthError::InvalidClient(format!( 212 "Failed to fetch JWKS: HTTP {}", 213 response.status() 214 ))); 215 } 216 let jwks: serde_json::Value = response 217 .json() 218 .await 219 .map_err(|e| OAuthError::InvalidClient(format!("Invalid JWKS JSON: {}", e)))?; 220 if jwks.get("keys").and_then(|k| k.as_array()).is_none() { 221 return Err(OAuthError::InvalidClient( 222 "JWKS must contain a 'keys' array".to_string(), 223 )); 224 } 225 Ok(jwks) 226 } 227 228 async fn fetch_metadata(&self, client_id: &str) -> Result<ClientMetadata, OAuthError> { 229 if !client_id.starts_with("http://") && !client_id.starts_with("https://") { 230 return Err(OAuthError::InvalidClient( 231 "client_id must be a URL".to_string(), 232 )); 233 } 234 if client_id.starts_with("http://") 235 && !client_id.contains("localhost") 236 && !client_id.contains("127.0.0.1") 237 { 238 return Err(OAuthError::InvalidClient( 239 "Non-localhost client_id must use https".to_string(), 240 )); 241 } 242 let response = self 243 .http_client 244 .get(client_id) 245 .header("Accept", "application/json") 246 .send() 247 .await 248 .map_err(|e| { 249 OAuthError::InvalidClient(format!("Failed to fetch client metadata: {}", e)) 250 })?; 251 if !response.status().is_success() { 252 return Err(OAuthError::InvalidClient(format!( 253 "Failed to fetch client metadata: HTTP {}", 254 response.status() 255 ))); 256 } 257 let mut metadata: ClientMetadata = response.json().await.map_err(|e| { 258 OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e)) 259 })?; 260 if metadata.client_id.is_empty() { 261 metadata.client_id = client_id.to_string(); 262 } else if metadata.client_id != client_id { 263 return Err(OAuthError::InvalidClient( 264 "client_id in metadata does not match request".to_string(), 265 )); 266 } 267 self.validate_metadata(&metadata)?; 268 Ok(metadata) 269 } 270 271 fn validate_metadata(&self, metadata: &ClientMetadata) -> Result<(), OAuthError> { 272 if metadata.redirect_uris.is_empty() { 273 return Err(OAuthError::InvalidClient( 274 "redirect_uris is required".to_string(), 275 )); 276 } 277 for uri in &metadata.redirect_uris { 278 self.validate_redirect_uri_format(uri)?; 279 } 280 if !metadata.grant_types.is_empty() 281 && !metadata 282 .grant_types 283 .contains(&"authorization_code".to_string()) 284 { 285 return Err(OAuthError::InvalidClient( 286 "authorization_code grant type is required".to_string(), 287 )); 288 } 289 if !metadata.response_types.is_empty() 290 && !metadata.response_types.contains(&"code".to_string()) 291 { 292 return Err(OAuthError::InvalidClient( 293 "code response type is required".to_string(), 294 )); 295 } 296 Ok(()) 297 } 298 299 pub fn validate_redirect_uri( 300 &self, 301 metadata: &ClientMetadata, 302 redirect_uri: &str, 303 ) -> Result<(), OAuthError> { 304 if metadata.redirect_uris.contains(&redirect_uri.to_string()) { 305 return Ok(()); 306 } 307 if Self::is_loopback_client(&metadata.client_id) 308 && let Ok(req_url) = reqwest::Url::parse(redirect_uri) 309 { 310 let req_host = req_url.host_str().unwrap_or(""); 311 let is_loopback_redirect = req_url.scheme() == "http" 312 && (req_host == "localhost" || req_host == "127.0.0.1" || req_host == "[::1]"); 313 if is_loopback_redirect { 314 return Ok(()); 315 } 316 } 317 Err(OAuthError::InvalidRequest( 318 "redirect_uri not registered for client".to_string(), 319 )) 320 } 321 322 fn validate_redirect_uri_format(&self, uri: &str) -> Result<(), OAuthError> { 323 if uri.contains('#') { 324 return Err(OAuthError::InvalidClient( 325 "redirect_uri must not contain a fragment".to_string(), 326 )); 327 } 328 let parsed = reqwest::Url::parse(uri) 329 .map_err(|_| OAuthError::InvalidClient(format!("Invalid redirect_uri: {}", uri)))?; 330 let scheme = parsed.scheme(); 331 if scheme == "http" { 332 let host = parsed.host_str().unwrap_or(""); 333 if host != "localhost" && host != "127.0.0.1" && host != "[::1]" { 334 return Err(OAuthError::InvalidClient( 335 "http redirect_uri only allowed for localhost".to_string(), 336 )); 337 } 338 } else if scheme == "https" { 339 } else if scheme.chars().all(|c| { 340 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '+' || c == '.' || c == '-' 341 }) { 342 if !scheme 343 .chars() 344 .next() 345 .map(|c| c.is_ascii_lowercase()) 346 .unwrap_or(false) 347 { 348 return Err(OAuthError::InvalidClient(format!( 349 "Invalid redirect_uri scheme: {}", 350 scheme 351 ))); 352 } 353 } else { 354 return Err(OAuthError::InvalidClient(format!( 355 "Invalid redirect_uri scheme: {}", 356 scheme 357 ))); 358 } 359 Ok(()) 360 } 361} 362 363impl ClientMetadata { 364 pub fn requires_dpop(&self) -> bool { 365 self.dpop_bound_access_tokens.unwrap_or(false) 366 } 367 368 pub fn auth_method(&self) -> &str { 369 self.token_endpoint_auth_method.as_deref().unwrap_or("none") 370 } 371} 372 373pub async fn verify_client_auth( 374 cache: &ClientMetadataCache, 375 metadata: &ClientMetadata, 376 client_auth: &super::ClientAuth, 377) -> Result<(), OAuthError> { 378 let expected_method = metadata.auth_method(); 379 match (expected_method, client_auth) { 380 ("none", super::ClientAuth::None) => Ok(()), 381 ("none", _) => Err(OAuthError::InvalidClient( 382 "Client is configured for no authentication, but credentials were provided".to_string(), 383 )), 384 ("private_key_jwt", super::ClientAuth::PrivateKeyJwt { client_assertion }) => { 385 verify_private_key_jwt_async(cache, metadata, client_assertion).await 386 } 387 ("private_key_jwt", _) => Err(OAuthError::InvalidClient( 388 "Client requires private_key_jwt authentication".to_string(), 389 )), 390 ("client_secret_post", super::ClientAuth::SecretPost { .. }) => { 391 Err(OAuthError::InvalidClient( 392 "client_secret_post is not supported for ATProto OAuth".to_string(), 393 )) 394 } 395 ("client_secret_basic", super::ClientAuth::SecretBasic { .. }) => { 396 Err(OAuthError::InvalidClient( 397 "client_secret_basic is not supported for ATProto OAuth".to_string(), 398 )) 399 } 400 (method, _) => Err(OAuthError::InvalidClient(format!( 401 "Unsupported or mismatched authentication method: {}", 402 method 403 ))), 404 } 405} 406 407async fn verify_private_key_jwt_async( 408 cache: &ClientMetadataCache, 409 metadata: &ClientMetadata, 410 client_assertion: &str, 411) -> Result<(), OAuthError> { 412 use base64::{ 413 Engine as _, 414 engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, 415 }; 416 let parts: Vec<&str> = client_assertion.split('.').collect(); 417 if parts.len() != 3 { 418 return Err(OAuthError::InvalidClient( 419 "Invalid client_assertion format".to_string(), 420 )); 421 } 422 let header_bytes = URL_SAFE_NO_PAD 423 .decode(parts[0]) 424 .or_else(|_| STANDARD.decode(parts[0])) 425 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header encoding".to_string()))?; 426 let header: serde_json::Value = serde_json::from_slice(&header_bytes) 427 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header JSON".to_string()))?; 428 let alg = header 429 .get("alg") 430 .and_then(|a| a.as_str()) 431 .ok_or_else(|| OAuthError::InvalidClient("Missing alg in client_assertion".to_string()))?; 432 if !matches!( 433 alg, 434 "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA" 435 ) { 436 return Err(OAuthError::InvalidClient(format!( 437 "Unsupported client_assertion algorithm: {}", 438 alg 439 ))); 440 } 441 let kid = header.get("kid").and_then(|k| k.as_str()); 442 let payload_bytes = URL_SAFE_NO_PAD 443 .decode(parts[1]) 444 .or_else(|_| STANDARD.decode(parts[1])) 445 .map_err(|e| { 446 tracing::warn!(error = %e, payload_part = parts[1], "Invalid assertion payload encoding"); 447 OAuthError::InvalidClient("Invalid assertion payload encoding".to_string()) 448 })?; 449 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes) 450 .map_err(|_| OAuthError::InvalidClient("Invalid assertion payload JSON".to_string()))?; 451 let iss = payload 452 .get("iss") 453 .and_then(|i| i.as_str()) 454 .ok_or_else(|| OAuthError::InvalidClient("Missing iss in client_assertion".to_string()))?; 455 if iss != metadata.client_id { 456 return Err(OAuthError::InvalidClient( 457 "client_assertion iss does not match client_id".to_string(), 458 )); 459 } 460 let sub = payload 461 .get("sub") 462 .and_then(|s| s.as_str()) 463 .ok_or_else(|| OAuthError::InvalidClient("Missing sub in client_assertion".to_string()))?; 464 if sub != metadata.client_id { 465 return Err(OAuthError::InvalidClient( 466 "client_assertion sub does not match client_id".to_string(), 467 )); 468 } 469 let now = chrono::Utc::now().timestamp(); 470 let exp = payload.get("exp").and_then(|e| e.as_i64()); 471 let iat = payload.get("iat").and_then(|i| i.as_i64()); 472 if let Some(exp) = exp { 473 if exp < now { 474 return Err(OAuthError::InvalidClient( 475 "client_assertion has expired".to_string(), 476 )); 477 } 478 } else if let Some(iat) = iat { 479 let max_age_secs = 300; 480 if now - iat > max_age_secs { 481 tracing::warn!( 482 iat = iat, 483 now = now, 484 "client_assertion too old (no exp, using iat)" 485 ); 486 return Err(OAuthError::InvalidClient( 487 "client_assertion is too old".to_string(), 488 )); 489 } 490 } else { 491 return Err(OAuthError::InvalidClient( 492 "client_assertion must have exp or iat claim".to_string(), 493 )); 494 } 495 if let Some(iat) = iat 496 && iat > now + 60 497 { 498 return Err(OAuthError::InvalidClient( 499 "client_assertion iat is in the future".to_string(), 500 )); 501 } 502 let jwks = cache.get_jwks(metadata).await?; 503 let keys = jwks 504 .get("keys") 505 .and_then(|k| k.as_array()) 506 .ok_or_else(|| OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string()))?; 507 let matching_keys: Vec<&serde_json::Value> = if let Some(kid) = kid { 508 keys.iter() 509 .filter(|k| k.get("kid").and_then(|v| v.as_str()) == Some(kid)) 510 .collect() 511 } else { 512 keys.iter().collect() 513 }; 514 if matching_keys.is_empty() { 515 return Err(OAuthError::InvalidClient( 516 "No matching key found in client JWKS".to_string(), 517 )); 518 } 519 let signing_input = format!("{}.{}", parts[0], parts[1]); 520 let signature_bytes = URL_SAFE_NO_PAD 521 .decode(parts[2]) 522 .map_err(|_| OAuthError::InvalidClient("Invalid signature encoding".to_string()))?; 523 for key in matching_keys { 524 let key_alg = key.get("alg").and_then(|a| a.as_str()); 525 if key_alg.is_some() && key_alg != Some(alg) { 526 continue; 527 } 528 let kty = key.get("kty").and_then(|k| k.as_str()).unwrap_or(""); 529 let verified = match (alg, kty) { 530 ("ES256", "EC") => verify_es256(key, &signing_input, &signature_bytes), 531 ("ES384", "EC") => verify_es384(key, &signing_input, &signature_bytes), 532 ("RS256" | "RS384" | "RS512", "RSA") => { 533 verify_rsa(alg, key, &signing_input, &signature_bytes) 534 } 535 ("EdDSA", "OKP") => verify_eddsa(key, &signing_input, &signature_bytes), 536 _ => continue, 537 }; 538 if verified.is_ok() { 539 return Ok(()); 540 } 541 } 542 Err(OAuthError::InvalidClient( 543 "client_assertion signature verification failed".to_string(), 544 )) 545} 546 547fn verify_es256( 548 key: &serde_json::Value, 549 signing_input: &str, 550 signature: &[u8], 551) -> Result<(), OAuthError> { 552 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 553 use p256::EncodedPoint; 554 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; 555 let x = key 556 .get("x") 557 .and_then(|v| v.as_str()) 558 .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?; 559 let y = key 560 .get("y") 561 .and_then(|v| v.as_str()) 562 .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?; 563 let x_bytes = URL_SAFE_NO_PAD 564 .decode(x) 565 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?; 566 let y_bytes = URL_SAFE_NO_PAD 567 .decode(y) 568 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?; 569 let mut point_bytes = vec![0x04]; 570 point_bytes.extend_from_slice(&x_bytes); 571 point_bytes.extend_from_slice(&y_bytes); 572 let point = EncodedPoint::from_bytes(&point_bytes) 573 .map_err(|_| OAuthError::InvalidClient("Invalid EC point".to_string()))?; 574 let verifying_key = VerifyingKey::from_encoded_point(&point) 575 .map_err(|_| OAuthError::InvalidClient("Invalid EC key".to_string()))?; 576 let sig = Signature::from_slice(signature) 577 .map_err(|_| OAuthError::InvalidClient("Invalid ES256 signature format".to_string()))?; 578 verifying_key 579 .verify(signing_input.as_bytes(), &sig) 580 .map_err(|_| OAuthError::InvalidClient("ES256 signature verification failed".to_string())) 581} 582 583fn verify_es384( 584 key: &serde_json::Value, 585 signing_input: &str, 586 signature: &[u8], 587) -> Result<(), OAuthError> { 588 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 589 use p384::EncodedPoint; 590 use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; 591 let x = key 592 .get("x") 593 .and_then(|v| v.as_str()) 594 .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?; 595 let y = key 596 .get("y") 597 .and_then(|v| v.as_str()) 598 .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?; 599 let x_bytes = URL_SAFE_NO_PAD 600 .decode(x) 601 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?; 602 let y_bytes = URL_SAFE_NO_PAD 603 .decode(y) 604 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?; 605 let mut point_bytes = vec![0x04]; 606 point_bytes.extend_from_slice(&x_bytes); 607 point_bytes.extend_from_slice(&y_bytes); 608 let point = EncodedPoint::from_bytes(&point_bytes) 609 .map_err(|_| OAuthError::InvalidClient("Invalid EC point".to_string()))?; 610 let verifying_key = VerifyingKey::from_encoded_point(&point) 611 .map_err(|_| OAuthError::InvalidClient("Invalid EC key".to_string()))?; 612 let sig = Signature::from_slice(signature) 613 .map_err(|_| OAuthError::InvalidClient("Invalid ES384 signature format".to_string()))?; 614 verifying_key 615 .verify(signing_input.as_bytes(), &sig) 616 .map_err(|_| OAuthError::InvalidClient("ES384 signature verification failed".to_string())) 617} 618 619fn verify_rsa( 620 _alg: &str, 621 _key: &serde_json::Value, 622 _signing_input: &str, 623 _signature: &[u8], 624) -> Result<(), OAuthError> { 625 Err(OAuthError::InvalidClient( 626 "RSA signature verification not yet supported - use EC keys".to_string(), 627 )) 628} 629 630fn verify_eddsa( 631 key: &serde_json::Value, 632 signing_input: &str, 633 signature: &[u8], 634) -> Result<(), OAuthError> { 635 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 636 use ed25519_dalek::{Signature, Verifier, VerifyingKey}; 637 let crv = key.get("crv").and_then(|c| c.as_str()).unwrap_or(""); 638 if crv != "Ed25519" { 639 return Err(OAuthError::InvalidClient(format!( 640 "Unsupported EdDSA curve: {}", 641 crv 642 ))); 643 } 644 let x = key 645 .get("x") 646 .and_then(|v| v.as_str()) 647 .ok_or_else(|| OAuthError::InvalidClient("Missing x in OKP key".to_string()))?; 648 let x_bytes = URL_SAFE_NO_PAD 649 .decode(x) 650 .map_err(|_| OAuthError::InvalidClient("Invalid x encoding".to_string()))?; 651 let key_bytes: [u8; 32] = x_bytes 652 .try_into() 653 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key length".to_string()))?; 654 let verifying_key = VerifyingKey::from_bytes(&key_bytes) 655 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key".to_string()))?; 656 let sig_bytes: [u8; 64] = signature 657 .try_into() 658 .map_err(|_| OAuthError::InvalidClient("Invalid EdDSA signature length".to_string()))?; 659 let sig = Signature::from_bytes(&sig_bytes); 660 verifying_key 661 .verify(signing_input.as_bytes(), &sig) 662 .map_err(|_| OAuthError::InvalidClient("EdDSA signature verification failed".to_string())) 663}