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