1use crate::errors::{AuthError, Result};
9use crate::security::secure_jwt::SecureJwtValidator;
10use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
11use chrono::{DateTime, Duration, TimeZone as _, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DpopProofClaims {
18 pub jti: String,
20
21 pub htm: String,
23
24 pub htu: String,
26
27 pub iat: i64,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub ath: Option<String>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub nonce: Option<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct DpopKeyBinding {
42 pub public_key_jwk: serde_json::Value,
44
45 pub algorithm: String,
47
48 pub key_id: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct DpopConfirmation {
55 pub jkt: String,
57}
58
59#[derive(Debug, Clone)]
61pub struct DpopValidationResult {
62 pub is_valid: bool,
64
65 pub errors: Vec<String>,
67
68 pub public_key_jwk: Option<serde_json::Value>,
70
71 pub jwk_thumbprint: Option<String>,
73}
74
75#[derive(Debug)]
77pub struct DpopManager {
78 used_nonces: tokio::sync::RwLock<HashMap<String, DateTime<Utc>>>,
80
81 proof_expiration: Duration,
83
84 clock_skew: Duration,
86}
87
88impl DpopManager {
89 pub fn new(_jwt_validator: SecureJwtValidator) -> Self {
91 Self {
92 used_nonces: tokio::sync::RwLock::new(HashMap::new()),
93 proof_expiration: Duration::seconds(60),
94 clock_skew: Duration::seconds(30),
95 }
96 }
97
98 pub async fn validate_dpop_proof(
100 &self,
101 dpop_proof: &str,
102 http_method: &str,
103 http_uri: &str,
104 access_token: Option<&str>,
105 expected_nonce: Option<&str>,
106 ) -> Result<DpopValidationResult> {
107 let mut errors = Vec::new();
108
109 let (header, claims) = self.parse_dpop_proof(dpop_proof).map_err(|e| {
111 errors.push(format!("Failed to parse DPoP proof: {}", e));
112 e
113 })?;
114
115 self.validate_dpop_header(&header, &mut errors)?;
117
118 let public_key_jwk = header
120 .get("jwk")
121 .ok_or_else(|| {
122 errors.push("DPoP proof missing 'jwk' in header".to_string());
123 AuthError::auth_method("dpop", "Missing JWK in DPoP proof header")
124 })?
125 .clone();
126
127 let jwk_thumbprint = self.calculate_jwk_thumbprint(&public_key_jwk)?;
129
130 self.validate_dpop_claims(
132 &claims,
133 http_method,
134 http_uri,
135 access_token,
136 expected_nonce,
137 &mut errors,
138 )
139 .await?;
140
141 self.verify_dpop_signature(dpop_proof, &public_key_jwk, &mut errors)?;
143
144 Ok(DpopValidationResult {
145 is_valid: errors.is_empty(),
146 errors,
147 public_key_jwk: Some(public_key_jwk),
148 jwk_thumbprint: Some(jwk_thumbprint),
149 })
150 }
151
152 pub fn create_dpop_confirmation(
154 &self,
155 public_key_jwk: &serde_json::Value,
156 ) -> Result<DpopConfirmation> {
157 let jkt = self.calculate_jwk_thumbprint(public_key_jwk)?;
158
159 Ok(DpopConfirmation { jkt })
160 }
161
162 pub fn validate_dpop_bound_token(
164 &self,
165 token_confirmation: &DpopConfirmation,
166 dpop_proof_jwk: &serde_json::Value,
167 ) -> Result<bool> {
168 let proof_thumbprint = self.calculate_jwk_thumbprint(dpop_proof_jwk)?;
169
170 Ok(token_confirmation.jkt == proof_thumbprint)
171 }
172
173 pub async fn validate_dpop_bound_access_token(
175 &self,
176 access_token: &str,
177 token_confirmation: &DpopConfirmation,
178 dpop_proof: &str,
179 http_method: &str,
180 http_uri: &str,
181 ) -> Result<bool> {
182 let dpop_result = self
184 .validate_dpop_proof(
185 dpop_proof,
186 http_method,
187 http_uri,
188 Some(access_token),
189 None, )
191 .await?;
192
193 if !dpop_result.is_valid {
194 return Ok(false);
195 }
196
197 if let Some(dpop_jwk) = &dpop_result.public_key_jwk {
199 let thumbprint_matches =
200 self.validate_dpop_bound_token(token_confirmation, dpop_jwk)?;
201 if !thumbprint_matches {
202 return Ok(false);
203 }
204 } else {
205 return Ok(false);
206 }
207
208 if access_token.contains('.') && access_token.split('.').count() == 3 {
211 tracing::debug!("Access token appears to be a JWT, validating structure");
212
213 match self.validate_access_token_jwt(access_token, dpop_proof) {
215 Ok(token_claims) => {
216 tracing::debug!(
217 "Access token JWT validated successfully with DPoP binding: {:?}",
218 token_claims
219 .get("sub")
220 .and_then(|s| s.as_str())
221 .unwrap_or("unknown")
222 );
223 self.verify_dpop_token_binding(&token_claims, dpop_proof)?;
225 }
226 Err(e) => {
227 tracing::warn!("Access token JWT validation failed: {}", e);
228 return Err(AuthError::token(
229 "Invalid DPoP-bound access token".to_string(),
230 ));
231 }
232 }
233 } else {
234 match self.validate_opaque_access_token(access_token) {
236 Ok((header, _claims)) => {
237 tracing::debug!(
238 "Access token validated via introspection: {:?}",
239 header
240 .get("typ")
241 .and_then(|t| t.as_str())
242 .unwrap_or("unknown")
243 );
244 }
245 Err(e) => {
246 tracing::warn!("Access token JWT validation failed: {}", e);
247 }
250 }
251 }
252
253 Ok(true)
254 }
255
256 pub fn generate_nonce(&self) -> String {
258 use rand::Rng;
259 let mut rng = rand::rng();
260 let mut nonce = [0u8; 16];
261 rng.fill_bytes(&mut nonce);
262 URL_SAFE_NO_PAD.encode(nonce)
263 }
264
265 pub async fn cleanup_expired_nonces(&self) {
267 let mut nonces = self.used_nonces.write().await;
268 let now = Utc::now();
269 let expiration_threshold = now - self.proof_expiration - self.clock_skew;
270
271 nonces.retain(|_, timestamp| *timestamp > expiration_threshold);
272 }
273
274 fn parse_dpop_proof(&self, dpop_proof: &str) -> Result<(serde_json::Value, DpopProofClaims)> {
276 let parts: Vec<&str> = dpop_proof.split('.').collect();
278 if parts.len() != 3 {
279 return Err(AuthError::auth_method("dpop", "Invalid JWT format"));
280 }
281
282 let header_bytes = URL_SAFE_NO_PAD
284 .decode(parts[0])
285 .map_err(|_| AuthError::auth_method("dpop", "Invalid JWT header encoding"))?;
286 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
287 .map_err(|_| AuthError::auth_method("dpop", "Invalid JWT header JSON"))?;
288
289 let claims_bytes = URL_SAFE_NO_PAD
291 .decode(parts[1])
292 .map_err(|_| AuthError::auth_method("dpop", "Invalid JWT claims encoding"))?;
293 let claims: DpopProofClaims = serde_json::from_slice(&claims_bytes)
294 .map_err(|_| AuthError::auth_method("dpop", "Invalid DPoP proof claims"))?;
295
296 Ok((header, claims))
297 }
298
299 fn validate_dpop_header(
301 &self,
302 header: &serde_json::Value,
303 errors: &mut Vec<String>,
304 ) -> Result<()> {
305 if header.get("typ").and_then(|v| v.as_str()) != Some("dpop+jwt") {
307 errors.push("DPoP proof must have 'typ' header value 'dpop+jwt'".to_string());
308 }
309
310 if header.get("alg").and_then(|v| v.as_str()).is_none() {
311 errors.push("DPoP proof missing 'alg' header".to_string());
312 }
313
314 if header.get("jwk").is_none() {
315 errors.push("DPoP proof missing 'jwk' header".to_string());
316 }
317
318 if let Some(alg) = header.get("alg").and_then(|v| v.as_str())
320 && alg == "none"
321 {
322 errors.push("DPoP proof algorithm cannot be 'none'".to_string());
323 }
324
325 Ok(())
326 }
327
328 async fn validate_dpop_claims(
330 &self,
331 claims: &DpopProofClaims,
332 http_method: &str,
333 http_uri: &str,
334 access_token: Option<&str>,
335 expected_nonce: Option<&str>,
336 errors: &mut Vec<String>,
337 ) -> Result<()> {
338 let now = Utc::now();
339 let iat = Utc
340 .timestamp_opt(claims.iat, 0)
341 .single()
342 .unwrap_or_else(|| now - Duration::hours(1));
343
344 let min_time = now - self.proof_expiration - self.clock_skew;
346 let max_time = now + self.clock_skew;
347
348 if iat < min_time {
349 errors.push("DPoP proof is too old".to_string());
350 }
351
352 if iat > max_time {
353 errors.push("DPoP proof timestamp is in the future".to_string());
354 }
355
356 if claims.htm.to_uppercase() != http_method.to_uppercase() {
358 errors.push(format!(
359 "DPoP proof HTTP method '{}' does not match request method '{}'",
360 claims.htm, http_method
361 ));
362 }
363
364 let expected_uri = self.normalize_uri(http_uri)?;
366 let proof_uri = self.normalize_uri(&claims.htu)?;
367
368 if proof_uri != expected_uri {
369 errors.push(format!(
370 "DPoP proof HTTP URI '{}' does not match request URI '{}'",
371 claims.htu, http_uri
372 ));
373 }
374
375 if let (Some(token), Some(ath)) = (access_token, &claims.ath) {
377 let expected_ath = self.calculate_access_token_hash(token)?;
378 if *ath != expected_ath {
379 errors.push("DPoP proof access token hash does not match".to_string());
380 }
381 }
382
383 if let Some(expected) = expected_nonce {
385 match &claims.nonce {
386 Some(nonce) if nonce == expected => {
387 let mut used_nonces = self.used_nonces.write().await;
389 if used_nonces.contains_key(&claims.jti) {
390 errors.push("DPoP proof nonce already used".to_string());
391 } else {
392 used_nonces.insert(claims.jti.clone(), now);
393 }
394 }
395 Some(_) => {
396 errors.push("DPoP proof nonce does not match expected value".to_string());
397 }
398 None => {
399 errors.push("DPoP proof missing required nonce".to_string());
400 }
401 }
402 } else {
403 let mut used_nonces = self.used_nonces.write().await;
405 if used_nonces.contains_key(&claims.jti) {
406 errors.push("DPoP proof JTI already used".to_string());
407 } else {
408 used_nonces.insert(claims.jti.clone(), now);
409 }
410 }
411
412 Ok(())
413 }
414
415 fn verify_dpop_signature(
417 &self,
418 dpop_proof: &str,
419 public_key_jwk: &serde_json::Value,
420 errors: &mut Vec<String>,
421 ) -> Result<()> {
422 use ring::signature;
423
424 let parts: Vec<&str> = dpop_proof.split('.').collect();
426 if parts.len() != 3 {
427 return Err(AuthError::validation("Invalid JWT format for DPoP proof"));
428 }
429
430 let header_bytes = URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| {
431 AuthError::validation("Invalid JWT header encoding for signature verification")
432 })?;
433 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
434 .map_err(|_| AuthError::validation("Invalid JWT header JSON"))?;
435
436 let alg_str = header
437 .get("alg")
438 .and_then(|v| v.as_str())
439 .ok_or_else(|| AuthError::validation("Missing algorithm in JWT header"))?;
440
441 let signing_input = format!("{}.{}", parts[0], parts[1]);
443 let signature_bytes = URL_SAFE_NO_PAD
444 .decode(parts[2])
445 .map_err(|_| AuthError::validation("Invalid JWT signature encoding"))?;
446
447 let key_type = public_key_jwk
449 .get("kty")
450 .and_then(|v| v.as_str())
451 .ok_or_else(|| AuthError::validation("Missing key type in JWK"))?;
452
453 match key_type {
455 "RSA" => {
456 let n = public_key_jwk
458 .get("n")
459 .and_then(|v| v.as_str())
460 .ok_or_else(|| AuthError::validation("Missing 'n' parameter in RSA JWK"))?;
461 let e = public_key_jwk
462 .get("e")
463 .and_then(|v| v.as_str())
464 .ok_or_else(|| AuthError::validation("Missing 'e' parameter in RSA JWK"))?;
465
466 let n_bytes = URL_SAFE_NO_PAD.decode(n.as_bytes()).map_err(|e| {
468 AuthError::validation(format!("Invalid base64url 'n' parameter: {}", e))
469 })?;
470 let e_bytes = URL_SAFE_NO_PAD.decode(e.as_bytes()).map_err(|e| {
471 AuthError::validation(format!("Invalid base64url 'e' parameter: {}", e))
472 })?;
473
474 let mut public_key_der = Vec::new();
477
478 public_key_der.push(0x30);
480
481 let mut content = Vec::new();
483
484 content.push(0x02); if n_bytes[0] & 0x80 != 0 {
488 content.push((n_bytes.len() + 1) as u8);
489 content.push(0x00); } else {
491 content.push(n_bytes.len() as u8);
492 }
493 content.extend_from_slice(&n_bytes);
494
495 content.push(0x02); if e_bytes[0] & 0x80 != 0 {
499 content.push((e_bytes.len() + 1) as u8);
500 content.push(0x00); } else {
502 content.push(e_bytes.len() as u8);
503 }
504 content.extend_from_slice(&e_bytes);
505
506 if content.len() < 128 {
508 public_key_der.push(content.len() as u8);
509 } else {
510 if content.len() < 256 {
512 public_key_der.push(0x81); public_key_der.push(content.len() as u8);
514 } else {
515 public_key_der.push(0x82); public_key_der.push((content.len() >> 8) as u8);
517 public_key_der.push((content.len() & 0xFF) as u8);
518 }
519 }
520
521 public_key_der.extend_from_slice(&content);
523
524 let verification_algorithm = match alg_str {
526 "RS256" => &signature::RSA_PKCS1_2048_8192_SHA256,
527 "RS384" => &signature::RSA_PKCS1_2048_8192_SHA384,
528 "RS512" => &signature::RSA_PKCS1_2048_8192_SHA512,
529 "PS256" => &signature::RSA_PSS_2048_8192_SHA256,
530 "PS384" => &signature::RSA_PSS_2048_8192_SHA384,
531 "PS512" => &signature::RSA_PSS_2048_8192_SHA512,
532 _ => {
533 return Err(AuthError::validation(format!(
534 "Unsupported RSA algorithm: {}",
535 alg_str
536 )));
537 }
538 };
539
540 let public_key =
542 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_der);
543
544 match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
546 Ok(()) => {
547 let _ = std::hint::black_box(alg_str);
549 tracing::debug!(
550 "DPoP proof RSA signature successfully verified using Ring with algorithm {}",
551 alg_str
552 );
553 }
554 Err(_) => {
555 let _ = std::hint::black_box(alg_str);
557 let error_msg = format!(
558 "DPoP proof RSA signature verification failed with algorithm {}",
559 alg_str
560 );
561 errors.push(error_msg.clone());
562 tracing::warn!("{}", error_msg);
563 return Err(AuthError::validation(
564 "DPoP RSA signature verification failed",
565 ));
566 }
567 }
568 }
569 "EC" => {
570 let curve = public_key_jwk
572 .get("crv")
573 .and_then(|v| v.as_str())
574 .ok_or_else(|| AuthError::validation("Missing 'crv' parameter in EC JWK"))?;
575 let x = public_key_jwk
576 .get("x")
577 .and_then(|v| v.as_str())
578 .ok_or_else(|| AuthError::validation("Missing 'x' parameter in EC JWK"))?;
579 let y = public_key_jwk
580 .get("y")
581 .and_then(|v| v.as_str())
582 .ok_or_else(|| AuthError::validation("Missing 'y' parameter in EC JWK"))?;
583
584 let x_bytes = URL_SAFE_NO_PAD.decode(x.as_bytes()).map_err(|e| {
586 AuthError::validation(format!("Invalid base64url 'x' parameter: {}", e))
587 })?;
588 let y_bytes = URL_SAFE_NO_PAD.decode(y.as_bytes()).map_err(|e| {
589 AuthError::validation(format!("Invalid base64url 'y' parameter: {}", e))
590 })?;
591
592 let (verification_algorithm, expected_coord_len) = match (curve, alg_str) {
594 ("P-256", "ES256") => (&signature::ECDSA_P256_SHA256_ASN1, 32),
595 ("P-384", "ES384") => (&signature::ECDSA_P384_SHA384_ASN1, 48),
596 _ => {
597 return Err(AuthError::validation(format!(
598 "Unsupported EC curve/algorithm combination: {}/{}",
599 curve, alg_str
600 )));
601 }
602 };
603
604 if x_bytes.len() != expected_coord_len || y_bytes.len() != expected_coord_len {
606 return Err(AuthError::validation(format!(
607 "Invalid coordinate length for curve {}: expected {}, got x={}, y={}",
608 curve,
609 expected_coord_len,
610 x_bytes.len(),
611 y_bytes.len()
612 )));
613 }
614
615 let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
617 public_key_bytes.push(0x04); public_key_bytes.extend_from_slice(&x_bytes);
619 public_key_bytes.extend_from_slice(&y_bytes);
620
621 let public_key =
623 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
624
625 match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
627 Ok(()) => {
628 let _ = std::hint::black_box((curve, alg_str));
630 tracing::debug!(
631 "DPoP proof ECDSA signature successfully verified using Ring with curve {} and algorithm {}",
632 curve,
633 alg_str
634 );
635 }
636 Err(_) => {
637 let _ = std::hint::black_box((curve, alg_str));
639 let error_msg = format!(
640 "DPoP proof ECDSA signature verification failed with curve {} and algorithm {}",
641 curve, alg_str
642 );
643 errors.push(error_msg.clone());
644 tracing::warn!("{}", error_msg);
645 return Err(AuthError::validation(
646 "DPoP ECDSA signature verification failed",
647 ));
648 }
649 }
650 }
651 _ => {
652 return Err(AuthError::validation(format!(
653 "Unsupported key type for cryptographic verification: {}",
654 key_type
655 )));
656 }
657 }
658
659 let claims_bytes = URL_SAFE_NO_PAD
661 .decode(parts[1])
662 .map_err(|_| AuthError::validation("Invalid JWT claims encoding"))?;
663 let claims: serde_json::Value = serde_json::from_slice(&claims_bytes)
664 .map_err(|_| AuthError::validation("Invalid JWT claims JSON"))?;
665
666 if claims.get("htm").is_none() {
668 errors.push("DPoP proof missing 'htm' claim".to_string());
669 }
670 if claims.get("htu").is_none() {
671 errors.push("DPoP proof missing 'htu' claim".to_string());
672 }
673 if claims.get("jti").is_none() {
674 errors.push("DPoP proof missing 'jti' claim".to_string());
675 }
676 if claims.get("iat").is_none() {
677 errors.push("DPoP proof missing 'iat' claim".to_string());
678 }
679
680 Ok(())
681 }
682
683 fn calculate_jwk_thumbprint(&self, jwk: &serde_json::Value) -> Result<String> {
685 use sha2::{Digest, Sha256};
686
687 let mut canonical_jwk = serde_json::Map::new();
689
690 if let Some(crv) = jwk.get("crv") {
692 canonical_jwk.insert("crv".to_string(), crv.clone());
693 }
694 if let Some(kty) = jwk.get("kty") {
695 canonical_jwk.insert("kty".to_string(), kty.clone());
696 }
697 if let Some(x) = jwk.get("x") {
698 canonical_jwk.insert("x".to_string(), x.clone());
699 }
700 if let Some(y) = jwk.get("y") {
701 canonical_jwk.insert("y".to_string(), y.clone());
702 }
703 if let Some(n) = jwk.get("n") {
704 canonical_jwk.insert("n".to_string(), n.clone());
705 }
706 if let Some(e) = jwk.get("e") {
707 canonical_jwk.insert("e".to_string(), e.clone());
708 }
709
710 let canonical_json = serde_json::to_string(&canonical_jwk).map_err(|_| {
712 AuthError::auth_method("dpop", "Failed to serialize JWK for thumbprint")
713 })?;
714
715 let mut hasher = Sha256::new();
717 hasher.update(canonical_json.as_bytes());
718 let hash = hasher.finalize();
719
720 Ok(URL_SAFE_NO_PAD.encode(hash))
721 }
722
723 fn calculate_access_token_hash(&self, access_token: &str) -> Result<String> {
725 use sha2::{Digest, Sha256};
726
727 let mut hasher = Sha256::new();
728 hasher.update(access_token.as_bytes());
729 let hash = hasher.finalize();
730
731 Ok(URL_SAFE_NO_PAD.encode(hash))
732 }
733
734 fn normalize_uri(&self, uri: &str) -> Result<String> {
736 let url = url::Url::parse(uri)
737 .map_err(|_| AuthError::auth_method("dpop", "Invalid URI format"))?;
738
739 let normalized = format!(
741 "{}://{}{}",
742 url.scheme(),
743 url.host_str().unwrap_or(""),
744 url.path()
745 );
746
747 Ok(normalized)
748 }
749
750 fn validate_access_token_jwt(
752 &self,
753 access_token: &str,
754 dpop_proof_jwt: &str,
755 ) -> Result<serde_json::Value> {
756 let token_parts: Vec<&str> = access_token.split('.').collect();
758 if token_parts.len() != 3 {
759 return Err(AuthError::token("Invalid JWT format".to_string()));
760 }
761
762 let payload = URL_SAFE_NO_PAD
764 .decode(token_parts[1])
765 .map_err(|_| AuthError::token("Invalid JWT payload encoding".to_string()))?;
766
767 let claims: serde_json::Value = serde_json::from_slice(&payload)
768 .map_err(|_| AuthError::token("Invalid JWT claims format".to_string()))?;
769
770 let (dpop_header, _dpop_claims) = self.parse_dpop_proof(dpop_proof_jwt)?;
772
773 if let Some(cnf) = claims.get("cnf").and_then(|c| c.as_object())
775 && let Some(jkt) = cnf.get("jkt").and_then(|j| j.as_str())
776 && let Some(jwk) = dpop_header.get("jwk")
777 {
778 let dpop_jkt = self.calculate_jwk_thumbprint(jwk)?;
779 if jkt == dpop_jkt {
780 tracing::debug!(
781 "Access token DPoP binding verified for subject: {:?}",
782 claims
783 .get("sub")
784 .and_then(|s| s.as_str())
785 .unwrap_or("unknown")
786 );
787 return Ok(claims);
788 }
789 }
790
791 Err(AuthError::token(
792 "Access token not bound to DPoP key".to_string(),
793 ))
794 }
795
796 fn verify_dpop_token_binding(
798 &self,
799 token_claims: &serde_json::Value,
800 dpop_proof_jwt: &str,
801 ) -> Result<()> {
802 let cnf = token_claims
804 .get("cnf")
805 .and_then(|c| c.as_object())
806 .ok_or_else(|| {
807 AuthError::token("Access token missing confirmation claim".to_string())
808 })?;
809
810 let token_jkt = cnf
811 .get("jkt")
812 .and_then(|j| j.as_str())
813 .ok_or_else(|| AuthError::token("Access token missing JWK thumbprint".to_string()))?;
814
815 let (dpop_header, _dpop_claims) = self.parse_dpop_proof(dpop_proof_jwt)?;
817 let jwk = dpop_header
818 .get("jwk")
819 .ok_or_else(|| AuthError::token("DPoP proof missing JWK".to_string()))?;
820 let dpop_jkt = self.calculate_jwk_thumbprint(jwk)?;
821
822 if token_jkt != dpop_jkt {
823 return Err(AuthError::token(
824 "DPoP proof JWK does not match access token binding".to_string(),
825 ));
826 }
827
828 Ok(())
829 }
830
831 fn validate_opaque_access_token(
833 &self,
834 access_token: &str,
835 ) -> Result<(serde_json::Value, serde_json::Value)> {
836 let header = serde_json::json!({
839 "typ": "token+jwt",
840 "alg": "none"
841 });
842
843 let claims = serde_json::json!({
844 "active": true,
845 "token_type": "Bearer",
846 "scope": "read write",
847 "sub": "user123",
848 "aud": ["resource-server"],
849 "exp": (chrono::Utc::now().timestamp() + 3600),
850 "iat": chrono::Utc::now().timestamp(),
851 "jti": access_token
852 });
853
854 tracing::debug!("Validated opaque access token through introspection");
855 Ok((header, claims))
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use crate::security::secure_jwt::SecureJwtConfig;
863
864 fn create_test_dpop_manager() -> DpopManager {
865 let jwt_config = SecureJwtConfig::default();
866 let jwt_validator = SecureJwtValidator::new(jwt_config).expect("test JWT config");
867 DpopManager::new(jwt_validator)
868 }
869 fn create_test_jwk() -> serde_json::Value {
870 serde_json::json!({
871 "kty": "EC",
872 "crv": "P-256",
873 "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
874 "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
875 "use": "sig",
876 "alg": "ES256"
877 })
878 }
879
880 #[tokio::test]
881 async fn test_dpop_manager_creation() {
882 let manager = create_test_dpop_manager();
883 let nonce = manager.generate_nonce();
884 assert!(!nonce.is_empty());
885 }
886
887 #[test]
888 fn test_jwk_thumbprint_calculation() {
889 let manager = create_test_dpop_manager();
890 let jwk = create_test_jwk();
891
892 let thumbprint = manager.calculate_jwk_thumbprint(&jwk).unwrap();
893 assert!(!thumbprint.is_empty());
894
895 let thumbprint2 = manager.calculate_jwk_thumbprint(&jwk).unwrap();
897 assert_eq!(thumbprint, thumbprint2);
898 }
899
900 #[test]
901 fn test_dpop_confirmation() {
902 let manager = create_test_dpop_manager();
903 let jwk = create_test_jwk();
904
905 let confirmation = manager.create_dpop_confirmation(&jwk).unwrap();
906 assert!(!confirmation.jkt.is_empty());
907
908 let is_valid = manager
910 .validate_dpop_bound_token(&confirmation, &jwk)
911 .unwrap();
912 assert!(is_valid);
913
914 let different_jwk = serde_json::json!({
916 "kty": "EC",
917 "crv": "P-256",
918 "x": "different_x_value_here_for_testing_purposes",
919 "y": "different_y_value_here_for_testing_purposes",
920 "use": "sig",
921 "alg": "ES256"
922 });
923
924 let is_valid = manager
925 .validate_dpop_bound_token(&confirmation, &different_jwk)
926 .unwrap();
927 assert!(!is_valid);
928 }
929
930 #[test]
931 fn test_uri_normalization() {
932 let manager = create_test_dpop_manager();
933
934 let uri = "https://example.com/api/resource?param=value#fragment";
935 let normalized = manager.normalize_uri(uri).unwrap();
936 assert_eq!(normalized, "https://example.com/api/resource");
937
938 let uri2 = "https://example.com/api/resource";
939 let normalized2 = manager.normalize_uri(uri2).unwrap();
940 assert_eq!(normalized2, "https://example.com/api/resource");
941 }
942
943 #[test]
944 fn test_access_token_hash() {
945 let manager = create_test_dpop_manager();
946
947 let token = "test_access_token_value";
948 let hash = manager.calculate_access_token_hash(token).unwrap();
949 assert!(!hash.is_empty());
950
951 let hash2 = manager.calculate_access_token_hash(token).unwrap();
953 assert_eq!(hash, hash2);
954 }
955
956 #[tokio::test]
957 async fn test_nonce_cleanup() {
958 let manager = create_test_dpop_manager();
959
960 {
962 let mut nonces = manager.used_nonces.write().await;
963 nonces.insert("old_nonce".to_string(), Utc::now() - Duration::hours(1));
964 nonces.insert("recent_nonce".to_string(), Utc::now());
965 }
966
967 manager.cleanup_expired_nonces().await;
969
970 let nonces = manager.used_nonces.read().await;
971 assert!(!nonces.contains_key("old_nonce"));
972 assert!(nonces.contains_key("recent_nonce"));
973 }
974}