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