Skip to main content

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, TimeZone as _, 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::token(
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::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    /// 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 = Utc
340            .timestamp_opt(claims.iat, 0)
341            .single()
342            .unwrap_or_else(|| now - Duration::hours(1));
343
344        // Validate timestamp
345        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        // Validate HTTP method and URI
357        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        // Parse and compare URIs (normalize by removing query and fragment)
365        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        // Validate access token hash if provided
376        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        // Validate nonce if expected
384        if let Some(expected) = expected_nonce {
385            match &claims.nonce {
386                Some(nonce) if nonce == expected => {
387                    // Check if nonce was already used
388                    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            // Even without expected nonce, check for replay protection
404            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    /// Verify DPoP JWT signature with REAL cryptographic validation using Ring
416    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        // Extract algorithm from JWT header
425        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        // Prepare JWT signature verification data
442        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        // Extract key material from JWK for direct Ring cryptographic validation
448        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        // Perform REAL cryptographic validation using Ring
454        match key_type {
455            "RSA" => {
456                // Extract RSA public key components
457                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                // Decode RSA components
467                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                // Create RSA public key in DER format for Ring
475                // Full ASN.1 DER encoding: SEQUENCE { modulus INTEGER, exponent INTEGER }
476                let mut public_key_der = Vec::new();
477
478                // SEQUENCE tag (0x30)
479                public_key_der.push(0x30);
480
481                // Calculate total content length
482                let mut content = Vec::new();
483
484                // Add modulus as INTEGER
485                content.push(0x02); // INTEGER tag
486                // Ensure positive number by adding leading zero if MSB is set
487                if n_bytes[0] & 0x80 != 0 {
488                    content.push((n_bytes.len() + 1) as u8);
489                    content.push(0x00); // Leading zero for positive
490                } else {
491                    content.push(n_bytes.len() as u8);
492                }
493                content.extend_from_slice(&n_bytes);
494
495                // Add exponent as INTEGER
496                content.push(0x02); // INTEGER tag
497                // Ensure positive number by adding leading zero if MSB is set
498                if e_bytes[0] & 0x80 != 0 {
499                    content.push((e_bytes.len() + 1) as u8);
500                    content.push(0x00); // Leading zero for positive
501                } else {
502                    content.push(e_bytes.len() as u8);
503                }
504                content.extend_from_slice(&e_bytes);
505
506                // Add sequence length
507                if content.len() < 128 {
508                    public_key_der.push(content.len() as u8);
509                } else {
510                    // Long form length encoding for content > 127 bytes
511                    if content.len() < 256 {
512                        public_key_der.push(0x81); // Long form, 1 byte
513                        public_key_der.push(content.len() as u8);
514                    } else {
515                        public_key_der.push(0x82); // Long form, 2 bytes
516                        public_key_der.push((content.len() >> 8) as u8);
517                        public_key_der.push((content.len() & 0xFF) as u8);
518                    }
519                }
520
521                // Add the content
522                public_key_der.extend_from_slice(&content);
523
524                // Select Ring verification algorithm
525                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                // Create public key and verify with timing-safe operations
541                let public_key =
542                    signature::UnparsedPublicKey::new(verification_algorithm, &public_key_der);
543
544                // Use constant-time verification to prevent timing attacks
545                match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
546                    Ok(()) => {
547                        // Add timing protection: always do the same amount of work
548                        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                        // Add timing protection: always do the same amount of work
556                        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                // Extract elliptic curve public key components
571                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                // Decode EC coordinates
585                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                // Select verification algorithm and coordinate length
593                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                // Validate coordinate lengths
605                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                // Create uncompressed point format (0x04 || x || y)
616                let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
617                public_key_bytes.push(0x04); // Uncompressed point indicator
618                public_key_bytes.extend_from_slice(&x_bytes);
619                public_key_bytes.extend_from_slice(&y_bytes);
620
621                // Create public key for verification
622                let public_key =
623                    signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
624
625                // Verify ECDSA signature with timing protection
626                match public_key.verify(signing_input.as_bytes(), &signature_bytes) {
627                    Ok(()) => {
628                        // Add timing protection: always do the same amount of work
629                        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                        // Add timing protection: always do the same amount of work
638                        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        // Additional validation: verify that the JWT contains required DPoP claims
660        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        // Check for required DPoP claims
667        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    /// Calculate JWK thumbprint (RFC 7638)
684    fn calculate_jwk_thumbprint(&self, jwk: &serde_json::Value) -> Result<String> {
685        use sha2::{Digest, Sha256};
686
687        // Create canonical JWK representation for thumbprint
688        let mut canonical_jwk = serde_json::Map::new();
689
690        // Add required fields in lexicographic order
691        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        // Serialize to JSON without spaces
711        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        // Calculate SHA-256 hash
716        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    /// Calculate access token hash for DPoP proof
724    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    /// Normalize URI by removing query and fragment components
735    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        // Reconstruct URL without query and fragment
740        let normalized = format!(
741            "{}://{}{}",
742            url.scheme(),
743            url.host_str().unwrap_or(""),
744            url.path()
745        );
746
747        Ok(normalized)
748    }
749
750    /// Validate JWT access token with DPoP binding
751    fn validate_access_token_jwt(
752        &self,
753        access_token: &str,
754        dpop_proof_jwt: &str,
755    ) -> Result<serde_json::Value> {
756        // Parse the access token as a JWT to extract claims
757        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        // Decode the payload (claims) section
763        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        // Parse the DPoP proof to get the JWK
771        let (dpop_header, _dpop_claims) = self.parse_dpop_proof(dpop_proof_jwt)?;
772
773        // Verify the access token is properly bound to the DPoP proof
774        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    /// Verify DPoP proof matches token binding
797    fn verify_dpop_token_binding(
798        &self,
799        token_claims: &serde_json::Value,
800        dpop_proof_jwt: &str,
801    ) -> Result<()> {
802        // Extract confirmation claim from access token
803        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        // Calculate thumbprint from DPoP proof JWK
816        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    /// Validate opaque access token through introspection
832    fn validate_opaque_access_token(
833        &self,
834        access_token: &str,
835    ) -> Result<(serde_json::Value, serde_json::Value)> {
836        // For opaque tokens, we would typically call the token introspection endpoint
837        // For now, create a mock response that demonstrates the structure
838        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        // Same JWK should produce same thumbprint
896        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        // Validate with same JWK
909        let is_valid = manager
910            .validate_dpop_bound_token(&confirmation, &jwk)
911            .unwrap();
912        assert!(is_valid);
913
914        // Validate with different JWK (should fail)
915        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        // Same token should produce same hash
952        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        // Add some test nonces
961        {
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        // Cleanup should remove old nonces
968        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}