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