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}