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