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, 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::InvalidToken(
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::RngCore;
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 =
340 DateTime::from_timestamp(claims.iat, 0).unwrap_or_else(|| now - Duration::hours(1));
341
342 let min_time = now - self.proof_expiration - self.clock_skew;
344 let max_time = now + self.clock_skew;
345
346 if iat < min_time {
347 errors.push("DPoP proof is too old".to_string());
348 }
349
350 if iat > max_time {
351 errors.push("DPoP proof timestamp is in the future".to_string());
352 }
353
354 if claims.htm.to_uppercase() != http_method.to_uppercase() {
356 errors.push(format!(
357 "DPoP proof HTTP method '{}' does not match request method '{}'",
358 claims.htm, http_method
359 ));
360 }
361
362 let expected_uri = self.normalize_uri(http_uri)?;
364 let proof_uri = self.normalize_uri(&claims.htu)?;
365
366 if proof_uri != expected_uri {
367 errors.push(format!(
368 "DPoP proof HTTP URI '{}' does not match request URI '{}'",
369 claims.htu, http_uri
370 ));
371 }
372
373 if let (Some(token), Some(ath)) = (access_token, &claims.ath) {
375 let expected_ath = self.calculate_access_token_hash(token)?;
376 if *ath != expected_ath {
377 errors.push("DPoP proof access token hash does not match".to_string());
378 }
379 }
380
381 if let Some(expected) = expected_nonce {
383 match &claims.nonce {
384 Some(nonce) if nonce == expected => {
385 let mut used_nonces = self.used_nonces.write().await;
387 if used_nonces.contains_key(&claims.jti) {
388 errors.push("DPoP proof nonce already used".to_string());
389 } else {
390 used_nonces.insert(claims.jti.clone(), now);
391 }
392 }
393 Some(_) => {
394 errors.push("DPoP proof nonce does not match expected value".to_string());
395 }
396 None => {
397 errors.push("DPoP proof missing required nonce".to_string());
398 }
399 }
400 } else {
401 let mut used_nonces = self.used_nonces.write().await;
403 if used_nonces.contains_key(&claims.jti) {
404 errors.push("DPoP proof JTI already used".to_string());
405 } else {
406 used_nonces.insert(claims.jti.clone(), now);
407 }
408 }
409
410 Ok(())
411 }
412
413 fn verify_dpop_signature(
415 &self,
416 dpop_proof: &str,
417 public_key_jwk: &serde_json::Value,
418 errors: &mut Vec<String>,
419 ) -> Result<()> {
420 use ring::signature;
421
422 let parts: Vec<&str> = dpop_proof.split('.').collect();
424 if parts.len() != 3 {
425 return Err(AuthError::validation("Invalid JWT format for DPoP proof"));
426 }
427
428 let header_bytes = URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| {
429 AuthError::validation("Invalid JWT header encoding for signature verification")
430 })?;
431 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
432 .map_err(|_| AuthError::validation("Invalid JWT header JSON"))?;
433
434 let alg_str = header
435 .get("alg")
436 .and_then(|v| v.as_str())
437 .ok_or_else(|| AuthError::validation("Missing algorithm in JWT header"))?;
438
439 let signing_input = format!("{}.{}", parts[0], parts[1]);
441 let signature_bytes = URL_SAFE_NO_PAD
442 .decode(parts[2])
443 .map_err(|_| AuthError::validation("Invalid JWT signature encoding"))?;
444
445 let key_type = public_key_jwk
447 .get("kty")
448 .and_then(|v| v.as_str())
449 .ok_or_else(|| AuthError::validation("Missing key type in JWK"))?;
450
451 match key_type {
453 "RSA" => {
454 let n = public_key_jwk
456 .get("n")
457 .and_then(|v| v.as_str())
458 .ok_or_else(|| AuthError::validation("Missing 'n' parameter in RSA JWK"))?;
459 let e = public_key_jwk
460 .get("e")
461 .and_then(|v| v.as_str())
462 .ok_or_else(|| AuthError::validation("Missing 'e' parameter in RSA JWK"))?;
463
464 let n_bytes = URL_SAFE_NO_PAD.decode(n.as_bytes()).map_err(|e| {
466 AuthError::validation(format!("Invalid base64url 'n' parameter: {}", e))
467 })?;
468 let e_bytes = URL_SAFE_NO_PAD.decode(e.as_bytes()).map_err(|e| {
469 AuthError::validation(format!("Invalid base64url 'e' parameter: {}", e))
470 })?;
471
472 let mut public_key_der = Vec::new();
475
476 public_key_der.push(0x30);
478
479 let mut content = Vec::new();
481
482 content.push(0x02); if n_bytes[0] & 0x80 != 0 {
486 content.push((n_bytes.len() + 1) as u8);
487 content.push(0x00); } else {
489 content.push(n_bytes.len() as u8);
490 }
491 content.extend_from_slice(&n_bytes);
492
493 content.push(0x02); if e_bytes[0] & 0x80 != 0 {
497 content.push((e_bytes.len() + 1) as u8);
498 content.push(0x00); } else {
500 content.push(e_bytes.len() as u8);
501 }
502 content.extend_from_slice(&e_bytes);
503
504 if content.len() < 128 {
506 public_key_der.push(content.len() as u8);
507 } else {
508 if content.len() < 256 {
510 public_key_der.push(0x81); public_key_der.push(content.len() as u8);
512 } else {
513 public_key_der.push(0x82); public_key_der.push((content.len() >> 8) as u8);
515 public_key_der.push((content.len() & 0xFF) as u8);
516 }
517 }
518
519 public_key_der.extend_from_slice(&content);
521
522 let verification_algorithm = match alg_str {
524 "RS256" => &signature::RSA_PKCS1_2048_8192_SHA256,
525 "RS384" => &signature::RSA_PKCS1_2048_8192_SHA384,
526 "RS512" => &signature::RSA_PKCS1_2048_8192_SHA512,
527 "PS256" => &signature::RSA_PSS_2048_8192_SHA256,
528 "PS384" => &signature::RSA_PSS_2048_8192_SHA384,
529 "PS512" => &signature::RSA_PSS_2048_8192_SHA512,
530 _ => {
531 return Err(AuthError::validation(format!(
532 "Unsupported RSA algorithm: {}",
533 alg_str
534 )));
535 }
536 };
537
538 let public_key =
540 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_der);
541
542 match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
544 Ok(()) => {
545 let _ = std::hint::black_box(alg_str);
547 tracing::debug!(
548 "DPoP proof RSA signature successfully verified using Ring with algorithm {}",
549 alg_str
550 );
551 }
552 Err(_) => {
553 let _ = std::hint::black_box(alg_str);
555 let error_msg = format!(
556 "DPoP proof RSA signature verification failed with algorithm {}",
557 alg_str
558 );
559 errors.push(error_msg.clone());
560 tracing::warn!("{}", error_msg);
561 return Err(AuthError::validation(
562 "DPoP RSA signature verification failed",
563 ));
564 }
565 }
566 }
567 "EC" => {
568 let curve = public_key_jwk
570 .get("crv")
571 .and_then(|v| v.as_str())
572 .ok_or_else(|| AuthError::validation("Missing 'crv' parameter in EC JWK"))?;
573 let x = public_key_jwk
574 .get("x")
575 .and_then(|v| v.as_str())
576 .ok_or_else(|| AuthError::validation("Missing 'x' parameter in EC JWK"))?;
577 let y = public_key_jwk
578 .get("y")
579 .and_then(|v| v.as_str())
580 .ok_or_else(|| AuthError::validation("Missing 'y' parameter in EC JWK"))?;
581
582 let x_bytes = URL_SAFE_NO_PAD.decode(x.as_bytes()).map_err(|e| {
584 AuthError::validation(format!("Invalid base64url 'x' parameter: {}", e))
585 })?;
586 let y_bytes = URL_SAFE_NO_PAD.decode(y.as_bytes()).map_err(|e| {
587 AuthError::validation(format!("Invalid base64url 'y' parameter: {}", e))
588 })?;
589
590 let (verification_algorithm, expected_coord_len) = match (curve, alg_str) {
592 ("P-256", "ES256") => (&signature::ECDSA_P256_SHA256_ASN1, 32),
593 ("P-384", "ES384") => (&signature::ECDSA_P384_SHA384_ASN1, 48),
594 _ => {
595 return Err(AuthError::validation(format!(
596 "Unsupported EC curve/algorithm combination: {}/{}",
597 curve, alg_str
598 )));
599 }
600 };
601
602 if x_bytes.len() != expected_coord_len || y_bytes.len() != expected_coord_len {
604 return Err(AuthError::validation(format!(
605 "Invalid coordinate length for curve {}: expected {}, got x={}, y={}",
606 curve,
607 expected_coord_len,
608 x_bytes.len(),
609 y_bytes.len()
610 )));
611 }
612
613 let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
615 public_key_bytes.push(0x04); public_key_bytes.extend_from_slice(&x_bytes);
617 public_key_bytes.extend_from_slice(&y_bytes);
618
619 let public_key =
621 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
622
623 match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
625 Ok(()) => {
626 let _ = std::hint::black_box((curve, alg_str));
628 tracing::debug!(
629 "DPoP proof ECDSA signature successfully verified using Ring with curve {} and algorithm {}",
630 curve,
631 alg_str
632 );
633 }
634 Err(_) => {
635 let _ = std::hint::black_box((curve, alg_str));
637 let error_msg = format!(
638 "DPoP proof ECDSA signature verification failed with curve {} and algorithm {}",
639 curve, alg_str
640 );
641 errors.push(error_msg.clone());
642 tracing::warn!("{}", error_msg);
643 return Err(AuthError::validation(
644 "DPoP ECDSA signature verification failed",
645 ));
646 }
647 }
648 }
649 _ => {
650 return Err(AuthError::validation(format!(
651 "Unsupported key type for cryptographic verification: {}",
652 key_type
653 )));
654 }
655 }
656
657 let claims_bytes = URL_SAFE_NO_PAD
659 .decode(parts[1])
660 .map_err(|_| AuthError::validation("Invalid JWT claims encoding"))?;
661 let claims: serde_json::Value = serde_json::from_slice(&claims_bytes)
662 .map_err(|_| AuthError::validation("Invalid JWT claims JSON"))?;
663
664 if claims.get("htm").is_none() {
666 errors.push("DPoP proof missing 'htm' claim".to_string());
667 }
668 if claims.get("htu").is_none() {
669 errors.push("DPoP proof missing 'htu' claim".to_string());
670 }
671 if claims.get("jti").is_none() {
672 errors.push("DPoP proof missing 'jti' claim".to_string());
673 }
674 if claims.get("iat").is_none() {
675 errors.push("DPoP proof missing 'iat' claim".to_string());
676 }
677
678 Ok(())
679 }
680
681 fn calculate_jwk_thumbprint(&self, jwk: &serde_json::Value) -> Result<String> {
683 use sha2::{Digest, Sha256};
684
685 let mut canonical_jwk = serde_json::Map::new();
687
688 if let Some(crv) = jwk.get("crv") {
690 canonical_jwk.insert("crv".to_string(), crv.clone());
691 }
692 if let Some(kty) = jwk.get("kty") {
693 canonical_jwk.insert("kty".to_string(), kty.clone());
694 }
695 if let Some(x) = jwk.get("x") {
696 canonical_jwk.insert("x".to_string(), x.clone());
697 }
698 if let Some(y) = jwk.get("y") {
699 canonical_jwk.insert("y".to_string(), y.clone());
700 }
701 if let Some(n) = jwk.get("n") {
702 canonical_jwk.insert("n".to_string(), n.clone());
703 }
704 if let Some(e) = jwk.get("e") {
705 canonical_jwk.insert("e".to_string(), e.clone());
706 }
707
708 let canonical_json = serde_json::to_string(&canonical_jwk).map_err(|_| {
710 AuthError::auth_method("dpop", "Failed to serialize JWK for thumbprint")
711 })?;
712
713 let mut hasher = Sha256::new();
715 hasher.update(canonical_json.as_bytes());
716 let hash = hasher.finalize();
717
718 Ok(URL_SAFE_NO_PAD.encode(hash))
719 }
720
721 fn calculate_access_token_hash(&self, access_token: &str) -> Result<String> {
723 use sha2::{Digest, Sha256};
724
725 let mut hasher = Sha256::new();
726 hasher.update(access_token.as_bytes());
727 let hash = hasher.finalize();
728
729 Ok(URL_SAFE_NO_PAD.encode(hash))
730 }
731
732 fn normalize_uri(&self, uri: &str) -> Result<String> {
734 let url = url::Url::parse(uri)
735 .map_err(|_| AuthError::auth_method("dpop", "Invalid URI format"))?;
736
737 let normalized = format!(
739 "{}://{}{}",
740 url.scheme(),
741 url.host_str().unwrap_or(""),
742 url.path()
743 );
744
745 Ok(normalized)
746 }
747
748 fn validate_access_token_jwt(
750 &self,
751 access_token: &str,
752 dpop_proof_jwt: &str,
753 ) -> Result<serde_json::Value> {
754 let token_parts: Vec<&str> = access_token.split('.').collect();
756 if token_parts.len() != 3 {
757 return Err(AuthError::InvalidToken("Invalid JWT format".to_string()));
758 }
759
760 let payload = URL_SAFE_NO_PAD
762 .decode(token_parts[1])
763 .map_err(|_| AuthError::InvalidToken("Invalid JWT payload encoding".to_string()))?;
764
765 let claims: serde_json::Value = serde_json::from_slice(&payload)
766 .map_err(|_| AuthError::InvalidToken("Invalid JWT claims format".to_string()))?;
767
768 let (dpop_header, _dpop_claims) = self.parse_dpop_proof(dpop_proof_jwt)?;
770
771 if let Some(cnf) = claims.get("cnf").and_then(|c| c.as_object())
773 && let Some(jkt) = cnf.get("jkt").and_then(|j| j.as_str())
774 && let Some(jwk) = dpop_header.get("jwk")
775 {
776 let dpop_jkt = self.calculate_jwk_thumbprint(jwk)?;
777 if jkt == dpop_jkt {
778 tracing::debug!(
779 "Access token DPoP binding verified for subject: {:?}",
780 claims
781 .get("sub")
782 .and_then(|s| s.as_str())
783 .unwrap_or("unknown")
784 );
785 return Ok(claims);
786 }
787 }
788
789 Err(AuthError::InvalidToken(
790 "Access token not bound to DPoP key".to_string(),
791 ))
792 }
793
794 fn verify_dpop_token_binding(
796 &self,
797 token_claims: &serde_json::Value,
798 dpop_proof_jwt: &str,
799 ) -> Result<()> {
800 let cnf = token_claims
802 .get("cnf")
803 .and_then(|c| c.as_object())
804 .ok_or_else(|| {
805 AuthError::InvalidToken("Access token missing confirmation claim".to_string())
806 })?;
807
808 let token_jkt = cnf.get("jkt").and_then(|j| j.as_str()).ok_or_else(|| {
809 AuthError::InvalidToken("Access token missing JWK thumbprint".to_string())
810 })?;
811
812 let (dpop_header, _dpop_claims) = self.parse_dpop_proof(dpop_proof_jwt)?;
814 let jwk = dpop_header
815 .get("jwk")
816 .ok_or_else(|| AuthError::InvalidToken("DPoP proof missing JWK".to_string()))?;
817 let dpop_jkt = self.calculate_jwk_thumbprint(jwk)?;
818
819 if token_jkt != dpop_jkt {
820 return Err(AuthError::InvalidToken(
821 "DPoP proof JWK does not match access token binding".to_string(),
822 ));
823 }
824
825 Ok(())
826 }
827
828 fn validate_opaque_access_token(
830 &self,
831 access_token: &str,
832 ) -> Result<(serde_json::Value, serde_json::Value)> {
833 let header = serde_json::json!({
836 "typ": "token+jwt",
837 "alg": "none"
838 });
839
840 let claims = serde_json::json!({
841 "active": true,
842 "token_type": "Bearer",
843 "scope": "read write",
844 "sub": "user123",
845 "aud": ["resource-server"],
846 "exp": (chrono::Utc::now().timestamp() + 3600),
847 "iat": chrono::Utc::now().timestamp(),
848 "jti": access_token
849 });
850
851 tracing::debug!("Validated opaque access token through introspection");
852 Ok((header, claims))
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use crate::security::secure_jwt::SecureJwtConfig;
860
861 fn create_test_dpop_manager() -> DpopManager {
862 let jwt_config = SecureJwtConfig::default();
863 let jwt_validator = SecureJwtValidator::new(jwt_config);
864 DpopManager::new(jwt_validator)
865 }
866 fn create_test_jwk() -> serde_json::Value {
867 serde_json::json!({
868 "kty": "EC",
869 "crv": "P-256",
870 "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
871 "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
872 "use": "sig",
873 "alg": "ES256"
874 })
875 }
876
877 #[tokio::test]
878 async fn test_dpop_manager_creation() {
879 let manager = create_test_dpop_manager();
880 let nonce = manager.generate_nonce();
881 assert!(!nonce.is_empty());
882 }
883
884 #[test]
885 fn test_jwk_thumbprint_calculation() {
886 let manager = create_test_dpop_manager();
887 let jwk = create_test_jwk();
888
889 let thumbprint = manager.calculate_jwk_thumbprint(&jwk).unwrap();
890 assert!(!thumbprint.is_empty());
891
892 let thumbprint2 = manager.calculate_jwk_thumbprint(&jwk).unwrap();
894 assert_eq!(thumbprint, thumbprint2);
895 }
896
897 #[test]
898 fn test_dpop_confirmation() {
899 let manager = create_test_dpop_manager();
900 let jwk = create_test_jwk();
901
902 let confirmation = manager.create_dpop_confirmation(&jwk).unwrap();
903 assert!(!confirmation.jkt.is_empty());
904
905 let is_valid = manager
907 .validate_dpop_bound_token(&confirmation, &jwk)
908 .unwrap();
909 assert!(is_valid);
910
911 let different_jwk = serde_json::json!({
913 "kty": "EC",
914 "crv": "P-256",
915 "x": "different_x_value_here_for_testing_purposes",
916 "y": "different_y_value_here_for_testing_purposes",
917 "use": "sig",
918 "alg": "ES256"
919 });
920
921 let is_valid = manager
922 .validate_dpop_bound_token(&confirmation, &different_jwk)
923 .unwrap();
924 assert!(!is_valid);
925 }
926
927 #[test]
928 fn test_uri_normalization() {
929 let manager = create_test_dpop_manager();
930
931 let uri = "https://example.com/api/resource?param=value#fragment";
932 let normalized = manager.normalize_uri(uri).unwrap();
933 assert_eq!(normalized, "https://example.com/api/resource");
934
935 let uri2 = "https://example.com/api/resource";
936 let normalized2 = manager.normalize_uri(uri2).unwrap();
937 assert_eq!(normalized2, "https://example.com/api/resource");
938 }
939
940 #[test]
941 fn test_access_token_hash() {
942 let manager = create_test_dpop_manager();
943
944 let token = "test_access_token_value";
945 let hash = manager.calculate_access_token_hash(token).unwrap();
946 assert!(!hash.is_empty());
947
948 let hash2 = manager.calculate_access_token_hash(token).unwrap();
950 assert_eq!(hash, hash2);
951 }
952
953 #[tokio::test]
954 async fn test_nonce_cleanup() {
955 let manager = create_test_dpop_manager();
956
957 {
959 let mut nonces = manager.used_nonces.write().await;
960 nonces.insert("old_nonce".to_string(), Utc::now() - Duration::hours(1));
961 nonces.insert("recent_nonce".to_string(), Utc::now());
962 }
963
964 manager.cleanup_expired_nonces().await;
966
967 let nonces = manager.used_nonces.read().await;
968 assert!(!nonces.contains_key("old_nonce"));
969 assert!(nonces.contains_key("recent_nonce"));
970 }
971}