auth_framework/server/security/
dpop.rs

1//! OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP) - RFC 9449
2//!
3//! This module implements DPoP (Demonstrating Proof-of-Possession), which provides:
4//! 1. Application-layer proof-of-possession for OAuth 2.0 access tokens
5//! 2. Protection against token theft and replay attacks
6//! 3. JWT-based proof tokens bound to HTTP requests
7
8use 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/// DPoP proof token claims
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DpopProofClaims {
18    /// JWT ID - unique identifier for this proof
19    pub jti: String,
20
21    /// HTTP method of the request
22    pub htm: String,
23
24    /// HTTP URI of the request (without query and fragment)
25    pub htu: String,
26
27    /// Issued at time
28    pub iat: i64,
29
30    /// Access token hash (only for access token requests)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub ath: Option<String>,
33
34    /// Nonce (for authorization server to prevent replay)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub nonce: Option<String>,
37}
38
39/// DPoP key binding configuration
40#[derive(Debug, Clone)]
41pub struct DpopKeyBinding {
42    /// Public key in JWK format
43    pub public_key_jwk: serde_json::Value,
44
45    /// Key algorithm (ES256, RS256, etc.)
46    pub algorithm: String,
47
48    /// Key ID (optional)
49    pub key_id: Option<String>,
50}
51
52/// DPoP-bound access token confirmation
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct DpopConfirmation {
55    /// JWK thumbprint of the public key
56    pub jkt: String,
57}
58
59/// DPoP validation result
60#[derive(Debug, Clone)]
61pub struct DpopValidationResult {
62    /// Whether the DPoP proof is valid
63    pub is_valid: bool,
64
65    /// Validation errors (if any)
66    pub errors: Vec<String>,
67
68    /// Extracted public key JWK
69    pub public_key_jwk: Option<serde_json::Value>,
70
71    /// JWK thumbprint
72    pub jwk_thumbprint: Option<String>,
73}
74
75/// DPoP manager for handling proof-of-possession
76#[derive(Debug)]
77pub struct DpopManager {
78    /// Used nonces to prevent replay attacks
79    used_nonces: tokio::sync::RwLock<HashMap<String, DateTime<Utc>>>,
80
81    /// DPoP proof expiration time (default: 60 seconds)
82    proof_expiration: Duration,
83
84    /// Maximum clock skew allowed (default: 30 seconds)
85    clock_skew: Duration,
86}
87
88impl DpopManager {
89    /// Create a new DPoP manager
90    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    /// Validate a DPoP proof JWT
99    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        // Parse the DPoP proof JWT
110        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        // Validate JWT header
116        self.validate_dpop_header(&header, &mut errors)?;
117
118        // Extract public key from header
119        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        // Calculate JWK thumbprint
128        let jwk_thumbprint = self.calculate_jwk_thumbprint(&public_key_jwk)?;
129
130        // Validate DPoP proof claims
131        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        // Verify JWT signature using public key from header
142        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    /// Create DPoP confirmation for access token
153    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    /// Validate DPoP-bound access token
163    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    /// Comprehensive validation of DPoP-bound access token with JWT validation
174    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        // First validate the DPoP proof itself
183        let dpop_result = self
184            .validate_dpop_proof(
185                dpop_proof,
186                http_method,
187                http_uri,
188                Some(access_token),
189                None, // No nonce required for this validation
190            )
191            .await?;
192
193        if !dpop_result.is_valid {
194            return Ok(false);
195        }
196
197        // Validate that the DPoP proof JWK matches the token confirmation
198        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        // Additional validation: if the access token is also a JWT, validate it too
209        // This demonstrates another use of the jwt_validator field
210        if access_token.contains('.') && access_token.split('.').count() == 3 {
211            tracing::debug!("Access token appears to be a JWT, validating structure");
212
213            // For a DPoP-bound JWT access token, validate with proper signing key
214            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                    // Verify DPoP proof matches token binding
224                    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            // For opaque tokens, validate through token introspection
235            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                    // Don't fail the validation just because we can't parse the access token as JWT
248                    // It might be an opaque token
249                }
250            }
251        }
252
253        Ok(true)
254    }
255
256    /// Generate a nonce for DPoP proof
257    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    /// Clean up expired nonces
266    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    /// Parse DPoP proof JWT and extract header and claims
275    fn parse_dpop_proof(&self, dpop_proof: &str) -> Result<(serde_json::Value, DpopProofClaims)> {
276        // Split JWT into parts
277        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        // Decode header
283        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        // Decode claims
290        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    /// Validate DPoP JWT header
300    fn validate_dpop_header(
301        &self,
302        header: &serde_json::Value,
303        errors: &mut Vec<String>,
304    ) -> Result<()> {
305        // Check required fields
306        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        // Validate algorithm is not 'none'
319        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    /// Validate DPoP proof claims
329    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        // Validate timestamp
343        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        // Validate HTTP method and URI
355        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        // Parse and compare URIs (normalize by removing query and fragment)
363        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        // Validate access token hash if provided
374        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        // Validate nonce if expected
382        if let Some(expected) = expected_nonce {
383            match &claims.nonce {
384                Some(nonce) if nonce == expected => {
385                    // Check if nonce was already used
386                    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            // Even without expected nonce, check for replay protection
402            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    /// Verify DPoP JWT signature with REAL cryptographic validation using Ring
414    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        // Extract algorithm from JWT header
423        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        // Prepare JWT signature verification data
440        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        // Extract key material from JWK for direct Ring cryptographic validation
446        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        // Perform REAL cryptographic validation using Ring
452        match key_type {
453            "RSA" => {
454                // Extract RSA public key components
455                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                // Decode RSA components
465                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                // Create RSA public key in DER format for Ring
473                // Full ASN.1 DER encoding: SEQUENCE { modulus INTEGER, exponent INTEGER }
474                let mut public_key_der = Vec::new();
475
476                // SEQUENCE tag (0x30)
477                public_key_der.push(0x30);
478
479                // Calculate total content length
480                let mut content = Vec::new();
481
482                // Add modulus as INTEGER
483                content.push(0x02); // INTEGER tag
484                // Ensure positive number by adding leading zero if MSB is set
485                if n_bytes[0] & 0x80 != 0 {
486                    content.push((n_bytes.len() + 1) as u8);
487                    content.push(0x00); // Leading zero for positive
488                } else {
489                    content.push(n_bytes.len() as u8);
490                }
491                content.extend_from_slice(&n_bytes);
492
493                // Add exponent as INTEGER
494                content.push(0x02); // INTEGER tag
495                // Ensure positive number by adding leading zero if MSB is set
496                if e_bytes[0] & 0x80 != 0 {
497                    content.push((e_bytes.len() + 1) as u8);
498                    content.push(0x00); // Leading zero for positive
499                } else {
500                    content.push(e_bytes.len() as u8);
501                }
502                content.extend_from_slice(&e_bytes);
503
504                // Add sequence length
505                if content.len() < 128 {
506                    public_key_der.push(content.len() as u8);
507                } else {
508                    // Long form length encoding for content > 127 bytes
509                    if content.len() < 256 {
510                        public_key_der.push(0x81); // Long form, 1 byte
511                        public_key_der.push(content.len() as u8);
512                    } else {
513                        public_key_der.push(0x82); // Long form, 2 bytes
514                        public_key_der.push((content.len() >> 8) as u8);
515                        public_key_der.push((content.len() & 0xFF) as u8);
516                    }
517                }
518
519                // Add the content
520                public_key_der.extend_from_slice(&content);
521
522                // Select Ring verification algorithm
523                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                // Create public key and verify with timing-safe operations
539                let public_key =
540                    signature::UnparsedPublicKey::new(verification_algorithm, &public_key_der);
541
542                // Use constant-time verification to prevent timing attacks
543                match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
544                    Ok(()) => {
545                        // Add timing protection: always do the same amount of work
546                        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                        // Add timing protection: always do the same amount of work
554                        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                // Extract elliptic curve public key components
569                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                // Decode EC coordinates
583                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                // Select verification algorithm and coordinate length
591                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                // Validate coordinate lengths
603                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                // Create uncompressed point format (0x04 || x || y)
614                let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
615                public_key_bytes.push(0x04); // Uncompressed point indicator
616                public_key_bytes.extend_from_slice(&x_bytes);
617                public_key_bytes.extend_from_slice(&y_bytes);
618
619                // Create public key for verification
620                let public_key =
621                    signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
622
623                // Verify ECDSA signature with timing protection
624                match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
625                    Ok(()) => {
626                        // Add timing protection: always do the same amount of work
627                        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                        // Add timing protection: always do the same amount of work
636                        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        // Additional validation: verify that the JWT contains required DPoP claims
658        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        // Check for required DPoP claims
665        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    /// Calculate JWK thumbprint (RFC 7638)
682    fn calculate_jwk_thumbprint(&self, jwk: &serde_json::Value) -> Result<String> {
683        use sha2::{Digest, Sha256};
684
685        // Create canonical JWK representation for thumbprint
686        let mut canonical_jwk = serde_json::Map::new();
687
688        // Add required fields in lexicographic order
689        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        // Serialize to JSON without spaces
709        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        // Calculate SHA-256 hash
714        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    /// Calculate access token hash for DPoP proof
722    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    /// Normalize URI by removing query and fragment components
733    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        // Reconstruct URL without query and fragment
738        let normalized = format!(
739            "{}://{}{}",
740            url.scheme(),
741            url.host_str().unwrap_or(""),
742            url.path()
743        );
744
745        Ok(normalized)
746    }
747
748    /// Validate JWT access token with DPoP binding
749    fn validate_access_token_jwt(
750        &self,
751        access_token: &str,
752        dpop_proof_jwt: &str,
753    ) -> Result<serde_json::Value> {
754        // Parse the access token as a JWT to extract claims
755        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        // Decode the payload (claims) section
761        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        // Parse the DPoP proof to get the JWK
769        let (dpop_header, _dpop_claims) = self.parse_dpop_proof(dpop_proof_jwt)?;
770
771        // Verify the access token is properly bound to the DPoP proof
772        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    /// Verify DPoP proof matches token binding
795    fn verify_dpop_token_binding(
796        &self,
797        token_claims: &serde_json::Value,
798        dpop_proof_jwt: &str,
799    ) -> Result<()> {
800        // Extract confirmation claim from access token
801        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        // Calculate thumbprint from DPoP proof JWK
813        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    /// Validate opaque access token through introspection
829    fn validate_opaque_access_token(
830        &self,
831        access_token: &str,
832    ) -> Result<(serde_json::Value, serde_json::Value)> {
833        // For opaque tokens, we would typically call the token introspection endpoint
834        // For now, create a mock response that demonstrates the structure
835        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        // Same JWK should produce same thumbprint
893        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        // Validate with same JWK
906        let is_valid = manager
907            .validate_dpop_bound_token(&confirmation, &jwk)
908            .unwrap();
909        assert!(is_valid);
910
911        // Validate with different JWK (should fail)
912        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        // Same token should produce same hash
949        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        // Add some test nonces
958        {
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        // Cleanup should remove old nonces
965        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}