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}