auth_framework/server/jwt/
jwt_introspection.rs

1//! RFC 9701 - JSON Web Token (JWT) Response for OAuth Token Introspection
2//!
3//! This module implements JWT-formatted responses for OAuth 2.0 token introspection
4//! as defined in RFC 9701.
5
6use crate::errors::{AuthError, Result};
7use chrono::{Duration, Utc};
8use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11use std::collections::HashMap;
12
13/// JWT introspection response claims as defined in RFC 9701
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct JwtIntrospectionClaims {
16    /// Issuer of the introspection response
17    pub iss: String,
18
19    /// Audience(s) for the introspection response
20    pub aud: Vec<String>,
21
22    /// Token identifier being introspected
23    pub jti: String,
24
25    /// Issued at time
26    pub iat: i64,
27
28    /// Expiration time of the introspection response
29    pub exp: i64,
30
31    /// Subject of the token being introspected
32    pub sub: Option<String>,
33
34    /// Client identifier
35    pub client_id: Option<String>,
36
37    /// Whether the token is active
38    pub active: bool,
39
40    /// Token type (e.g., "access_token", "refresh_token")
41    pub token_type: Option<String>,
42
43    /// Scope values associated with the token
44    pub scope: Option<String>,
45
46    /// Username of the resource owner
47    pub username: Option<String>,
48
49    /// Expiration time of the token being introspected
50    pub token_exp: Option<i64>,
51
52    /// Issued at time of the token being introspected
53    pub token_iat: Option<i64>,
54
55    /// Not before time of the token being introspected
56    pub token_nbf: Option<i64>,
57
58    /// Audience of the token being introspected
59    pub token_aud: Option<Vec<String>>,
60
61    /// Issuer of the token being introspected
62    pub token_iss: Option<String>,
63
64    /// Additional claims from the original token
65    #[serde(flatten)]
66    pub additional_claims: HashMap<String, Value>,
67}
68
69/// Basic introspection response (RFC 7662)
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BasicIntrospectionResponse {
72    /// Whether the token is active
73    pub active: bool,
74
75    /// Scope values associated with the token
76    pub scope: Option<String>,
77
78    /// Client identifier
79    pub client_id: Option<String>,
80
81    /// Username of the resource owner
82    pub username: Option<String>,
83
84    /// Token type
85    pub token_type: Option<String>,
86
87    /// Expiration time
88    pub exp: Option<i64>,
89
90    /// Issued at time
91    pub iat: Option<i64>,
92
93    /// Not before time
94    pub nbf: Option<i64>,
95
96    /// Subject
97    pub sub: Option<String>,
98
99    /// Audience
100    pub aud: Option<Vec<String>>,
101
102    /// Issuer
103    pub iss: Option<String>,
104
105    /// Token identifier
106    pub jti: Option<String>,
107
108    /// Additional claims
109    #[serde(flatten)]
110    pub additional_claims: HashMap<String, Value>,
111}
112
113/// Configuration for JWT introspection responses
114#[derive(Debug, Clone)]
115pub struct JwtIntrospectionConfig {
116    /// Issuer identifier for introspection responses
117    pub issuer: String,
118
119    /// Default audience for introspection responses
120    pub default_audience: Vec<String>,
121
122    /// Expiration time for introspection responses (seconds)
123    pub response_expiration: i64,
124
125    /// Algorithm for signing introspection responses
126    pub signing_algorithm: Algorithm,
127
128    /// Whether to include the original token claims
129    pub include_token_claims: bool,
130
131    /// Whether to validate the audience in the introspection request
132    pub validate_audience: bool,
133}
134
135impl Default for JwtIntrospectionConfig {
136    fn default() -> Self {
137        Self {
138            issuer: "https://auth.example.com".to_string(),
139            default_audience: vec!["https://api.example.com".to_string()],
140            response_expiration: 300, // 5 minutes
141            signing_algorithm: Algorithm::HS256,
142            include_token_claims: true,
143            validate_audience: true,
144        }
145    }
146}
147
148/// JWT Token Introspection Manager
149pub struct JwtIntrospectionManager {
150    config: JwtIntrospectionConfig,
151    private_key: EncodingKey,
152    public_key: DecodingKey,
153}
154
155impl JwtIntrospectionManager {
156    /// Create a new JWT introspection manager
157    pub fn new(config: JwtIntrospectionConfig) -> Result<Self> {
158        // Generate a default key pair for demonstration
159        // In production, use proper key management
160        let key_bytes = b"introspection_jwt_secret_key_change_in_production";
161        let private_key = EncodingKey::from_secret(key_bytes);
162        let public_key = DecodingKey::from_secret(key_bytes);
163
164        Ok(Self {
165            config,
166            private_key,
167            public_key,
168        })
169    }
170
171    /// Create a JWT introspection response from basic introspection data
172    pub fn create_jwt_response(
173        &self,
174        basic_response: BasicIntrospectionResponse,
175        audience: Option<Vec<String>>,
176        token_jti: Option<String>,
177    ) -> Result<String> {
178        let now = Utc::now();
179        let exp = now + Duration::seconds(self.config.response_expiration);
180
181        let claims = JwtIntrospectionClaims {
182            iss: self.config.issuer.clone(),
183            aud: audience.unwrap_or_else(|| self.config.default_audience.clone()),
184            jti: token_jti.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
185            iat: now.timestamp(),
186            exp: exp.timestamp(),
187            sub: basic_response.sub,
188            client_id: basic_response.client_id,
189            active: basic_response.active,
190            token_type: basic_response.token_type,
191            scope: basic_response.scope,
192            username: basic_response.username,
193            token_exp: basic_response.exp,
194            token_iat: basic_response.iat,
195            token_nbf: basic_response.nbf,
196            token_aud: basic_response.aud,
197            token_iss: basic_response.iss,
198            additional_claims: basic_response.additional_claims,
199        };
200
201        let header = Header::new(self.config.signing_algorithm);
202        let token = jsonwebtoken::encode(&header, &claims, &self.private_key).map_err(|e| {
203            AuthError::crypto(format!(
204                "Failed to create JWT introspection response: {}",
205                e
206            ))
207        })?;
208
209        Ok(token)
210    }
211
212    /// Verify and parse a JWT introspection response
213    pub fn verify_jwt_response(&self, jwt_token: &str) -> Result<JwtIntrospectionClaims> {
214        let mut validation = Validation::new(self.config.signing_algorithm);
215        validation.set_issuer(&[&self.config.issuer]);
216
217        if self.config.validate_audience {
218            validation.set_audience(&self.config.default_audience);
219        } else {
220            validation.validate_aud = false;
221        }
222
223        let token_data = jsonwebtoken::decode::<JwtIntrospectionClaims>(
224            jwt_token,
225            &self.public_key,
226            &validation,
227        )
228        .map_err(|e| {
229            AuthError::crypto(format!(
230                "Failed to verify JWT introspection response: {}",
231                e
232            ))
233        })?;
234
235        Ok(token_data.claims)
236    }
237
238    /// Create an inactive token response (for expired or invalid tokens)
239    pub fn create_inactive_response(
240        &self,
241        audience: Option<Vec<String>>,
242        token_jti: Option<String>,
243    ) -> Result<String> {
244        let basic_response = BasicIntrospectionResponse {
245            active: false,
246            scope: None,
247            client_id: None,
248            username: None,
249            token_type: None,
250            exp: None,
251            iat: None,
252            nbf: None,
253            sub: None,
254            aud: None,
255            iss: None,
256            jti: None,
257            additional_claims: HashMap::new(),
258        };
259
260        self.create_jwt_response(basic_response, audience, token_jti)
261    }
262
263    /// Convert JWT introspection claims back to basic response format
264    pub fn jwt_to_basic_response(
265        &self,
266        claims: &JwtIntrospectionClaims,
267    ) -> BasicIntrospectionResponse {
268        BasicIntrospectionResponse {
269            active: claims.active,
270            scope: claims.scope.clone(),
271            client_id: claims.client_id.clone(),
272            username: claims.username.clone(),
273            token_type: claims.token_type.clone(),
274            exp: claims.token_exp,
275            iat: claims.token_iat,
276            nbf: claims.token_nbf,
277            sub: claims.sub.clone(),
278            aud: claims.token_aud.clone(),
279            iss: claims.token_iss.clone(),
280            jti: Some(claims.jti.clone()),
281            additional_claims: claims.additional_claims.clone(),
282        }
283    }
284
285    /// Validate introspection request audience
286    pub fn validate_request_audience(&self, requested_audience: &[String]) -> bool {
287        if !self.config.validate_audience {
288            return true;
289        }
290
291        // Check if any requested audience is in our allowed audiences
292        requested_audience
293            .iter()
294            .any(|aud| self.config.default_audience.contains(aud))
295    }
296
297    /// Get the issuer for introspection responses
298    pub fn get_issuer(&self) -> &str {
299        &self.config.issuer
300    }
301
302    /// Get the default audience
303    pub fn get_default_audience(&self) -> &[String] {
304        &self.config.default_audience
305    }
306
307    /// Create an error response for invalid requests
308    pub fn create_error_response(&self, error: &str, error_description: Option<&str>) -> Value {
309        let mut response = json!({
310            "error": error,
311            "active": false
312        });
313
314        if let Some(description) = error_description {
315            response["error_description"] = json!(description);
316        }
317
318        response
319    }
320
321    /// Create introspection metadata for discovery
322    pub fn create_introspection_metadata(&self) -> Value {
323        json!({
324            "introspection_endpoint": format!("{}/introspect", self.config.issuer),
325            "introspection_endpoint_auth_methods_supported": [
326                "client_secret_basic",
327                "client_secret_post",
328                "private_key_jwt"
329            ],
330            "introspection_endpoint_auth_signing_alg_values_supported": [
331                "RS256", "RS384", "RS512",
332                "ES256", "ES384", "ES512",
333                "PS256", "PS384", "PS512"
334            ],
335            "introspection_signing_alg_values_supported": [
336                format!("{:?}", self.config.signing_algorithm)
337            ],
338            "introspection_response_format": "jwt"
339        })
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use std::collections::HashMap;
347
348    #[test]
349    fn test_jwt_introspection_response_creation() {
350        let config = JwtIntrospectionConfig::default();
351        let manager = JwtIntrospectionManager::new(config).unwrap();
352
353        let basic_response = BasicIntrospectionResponse {
354            active: true,
355            scope: Some("read write".to_string()),
356            client_id: Some("test_client".to_string()),
357            username: Some("user123".to_string()),
358            token_type: Some("access_token".to_string()),
359            exp: Some(Utc::now().timestamp() + 3600),
360            iat: Some(Utc::now().timestamp()),
361            nbf: None,
362            sub: Some("user123".to_string()),
363            aud: Some(vec!["https://api.example.com".to_string()]),
364            iss: Some("https://auth.example.com".to_string()),
365            jti: Some("token123".to_string()),
366            additional_claims: HashMap::new(),
367        };
368
369        let jwt_response = manager
370            .create_jwt_response(
371                basic_response,
372                Some(vec!["https://api.example.com".to_string()]),
373                Some("introspection123".to_string()),
374            )
375            .unwrap();
376
377        assert!(!jwt_response.is_empty());
378        assert!(jwt_response.split('.').count() == 3); // Valid JWT format
379    }
380
381    #[test]
382    fn test_jwt_introspection_verification() {
383        let config = JwtIntrospectionConfig::default();
384        let manager = JwtIntrospectionManager::new(config).unwrap();
385
386        let basic_response = BasicIntrospectionResponse {
387            active: true,
388            scope: Some("read".to_string()),
389            client_id: Some("test_client".to_string()),
390            username: Some("user123".to_string()),
391            token_type: Some("access_token".to_string()),
392            exp: Some(Utc::now().timestamp() + 3600),
393            iat: Some(Utc::now().timestamp()),
394            nbf: None,
395            sub: Some("user123".to_string()),
396            aud: Some(vec!["https://api.example.com".to_string()]),
397            iss: Some("https://auth.example.com".to_string()),
398            jti: Some("token123".to_string()),
399            additional_claims: HashMap::new(),
400        };
401
402        let jwt_response = manager
403            .create_jwt_response(basic_response.clone(), None, None)
404            .unwrap();
405
406        let verified_claims = manager.verify_jwt_response(&jwt_response).unwrap();
407
408        assert_eq!(verified_claims.active, basic_response.active);
409        assert_eq!(verified_claims.scope, basic_response.scope);
410        assert_eq!(verified_claims.client_id, basic_response.client_id);
411        assert_eq!(verified_claims.username, basic_response.username);
412    }
413
414    #[test]
415    fn test_inactive_token_response() {
416        let config = JwtIntrospectionConfig::default();
417        let manager = JwtIntrospectionManager::new(config).unwrap();
418
419        let jwt_response = manager.create_inactive_response(None, None).unwrap();
420        let verified_claims = manager.verify_jwt_response(&jwt_response).unwrap();
421
422        assert!(!verified_claims.active);
423        assert!(verified_claims.scope.is_none());
424        assert!(verified_claims.client_id.is_none());
425    }
426
427    #[test]
428    fn test_audience_validation() {
429        let config = JwtIntrospectionConfig::default();
430        let manager = JwtIntrospectionManager::new(config).unwrap();
431
432        let valid_audience = vec!["https://api.example.com".to_string()];
433        assert!(manager.validate_request_audience(&valid_audience));
434
435        let invalid_audience = vec!["https://malicious.example.com".to_string()];
436        assert!(!manager.validate_request_audience(&invalid_audience));
437    }
438}
439
440