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