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